2011/11/23

Corona SDKでのDrag & Drop基本サンプル

新しく取りかかり始めたアプリでタッチ操作によるDrag&Dropを実装しようと思い、以前にclipしておいたこのページのDrag&Dropのサンプルコードを動かしてみました。
-- create object
local myObject = display.newRect( 0, 0, 100, 100 )
myObject:setFillColor( 255 )

-- touch listener function
function myObject:touch( event )
  if event.phase == "began" then
    self.markX = self.x    -- store x location of object
    self.markY = self.y    -- store y location of object
  elseif event.phase == "moved" then
    local x = (event.x - event.xStart) + self.markX
    local y = (event.y - event.yStart) + self.markY
    self.x, self.y = x, y    -- move object based on calculations above
  end
  return true
end

-- make 'myObject' listen for touch events
myObject:addEventListener( "touch", myObject )
実際に動かしてみるとわかるのですが、タッチ操作(指の動き)にオブジェクトが追従してきません。最初はfpsが低いのかなとも思ったのですが、60fpsに設定しても変わりませんでした。

ソース的には何も間違っていないような印象があったのですが、別のサンプルコードを見つけて納得しました。上記のコードでは指の動きが少しでも早くなると、オブジェクトに対するイベントがすぐに外れてしまうため、操作に対する追従が悪かったのです。

実践的というか使えるコードとしては以下の様になります。
-- create object
local myObject = display.newRect( 0, 0, 100, 100 )
myObject:setFillColor( 255 )

-- touch listener function
function myObject:touch( event )
  local t = event.target
  local phase = event.phase
  if("began" == phase) then
    display.getCurrentStage():setFocus(t)
    t.isFocus = true
    t.x0 = event.x - t.x
    t.y0 = event.y - t.y
  elseif(t.isFocus) then
    if("moved" == phase) then
      t.x = event.x - t.x0
      t.y = event.y - t.y0
    elseif("ended" == phase or "cancelled" == phase) then
      display.getCurrentStage():setFocus(nil)
      t.isFocus = false
    end
  end
  return true
end

-- make 'myObject' listen for touch events
myObject:addEventListener( "touch", myObject )
オブジェクトに対する操作(イベント)が終わるまでフォーカスが外れないようになっています。
これで指をグリグリ動かしてもオブジェクトが追従してくれるようになりました。

せっかくタッチパネル向けにアプリをつくるなら、ボタンをタップするだけじゃなくて、指で操作させたい(したい)ですよね。

2011/09/17

Director ClassとAndroidのKEYイベント[Corona SDK]

Corona SDKでAndroidのKEYイベントを取得し、Backキーでひとつ前の画面に戻ったり、Menuボタンで設定画面に遷移したりといった処理を行おうとした際に、Director Classを使っている場合には、ちょっとした注意が必要です。(もちろん私もハマりましたよw)

先日リリースした「Cutie Horoscope」でもAndroidではキー操作によって“戻る”と“メニュー画面を表示”を行っています。
※実際の動きは、アプリをDLして確認ください。

最初はアンスカのAPI解説ページを見て、取得したKEYイベントにあわせてdirector:changeScene()で画面遷移できるんだろうと思って、そのようなコードを書いてみたのですが、画面遷移のアニメーションが効かないという状況に。。。

あれ?

この時すでにライセンスを購入済みだったのでForumを検索してみると、あっ、あった!
「Android device back button utilizing Director class」というタイトルのトピックが立てられていました。

あー、色々と小細工的な処理が必要なようです。
※Forum見られる方はそちらへ。サンプルのソースが載ってます

下記に「Cutie Horoscope」のソースを抜粋して解説します。





new = function ( params )
  
  --(略)--
  
  local localGroup = display.newGroup()
  
  --ポイント1--
  local backButtonPushed = false
  local menuButtonPushed = false
  
  --(略)--
  
  --ポイント2--
  local animate = function( event )
    if backButtonPushed == true then
      backButtonPushed = false
      os.exit()
    elseif menuButtonPushed == true then
      menuButtonPushed = false
      director:changeScene( "setting", "overFromBottom" )
    end
  end

  local onKeyEvent = function( event )
    local phase = event.phase
    local keyName = event.keyName
    
    if phase == "up" and (keyName == "back") then
      backButtonPushed = true
    elseif phase == "up" and (keyName == "menu") then
      menuButtonPushed = true
    end
    
    return true
  end
  
  --(略)--
  
  --ポイント3--
  function clean ( event )
    Runtime:removeEventListener( "key", onKeyEvent )
    Runtime:removeEventListener( "enterFrame", animate )
    backButtonPushed = nil
    menuButtonPushed = nil
  end
  
  local initVars = function ()
    
    --(略)--
    
    --ポイント2--
    Runtime:addEventListener( "key", onKeyEvent )
    Runtime:addEventListener( "enterFrame", animate )
    
    --(略)--
    
  end
  
  initVars()
  
  return localGroup
  
end

ポイントは3つ
1.ボタンが押されたことを判別するフラグを用意する
2.enterFrameイベントでそのフラグを判定して遷移等の処理を行う。KEYイベントはどのキーが押されたか、フラグを書き換えるだけ
3.cleanメソッドでイベントリスナーとフラグの解除を行う

上記の様にしないと、遷移時のエフェクトが掛かりません。そして、イベントの解除等をしないと、最初に設定したイベントが有効のままになってしまうようで、Backキーを押した時に戻る画面が違って何故?ってことになります。

動かし方がわかれば、後はキーと遷移時のエフェクトの組み合わせをうまくやれば、ネイティブ・アプリ(Javaで開発したアプリ)のような動作が可能になります。

2011/09/16

Corona SDKでアプリをリリースする際に起こった「えっ!?」ってこと

Corona SDKでのアプリ開発では、色々と問題も起きます。今回はちょっとキビシイ現実を書きます。

下記は「Cutie Horoscope」のbuild.settingsファイルの中身です。

settings =
{
  iphone =
  {
    plist =
    {
      MinimumOSVersion="4.0",
    },
  },
  android =
  {
    versionCode="2",
    versionName="1.1",
    installLocation="preferExternal",
  },
}

まず、iOS向けに「MinimumOSVersion="4.0"」が設定してあります。
これはiTunes Connectにファイルを送信し終えた時に、たまたまBinary Detailsをクリックすると、なんということでしょう「Minimum OS Requirements」が「3.1」に、そして「Supported Architecture」が「armv6, armv7」になっているではありませんか!
「Coronaは4以上で固定ではなかったのかよ〜」と、あわててファイルを削除し、「MinimumOSVersion="4.0"」を加えることによって回避することにしました。実際に最初にUPした設定のファイルをオーガナイザ経由で第1世代のiPod touch(iOS 3.1.3、armv6)の端末にインストールすることが可能でした。(起動はしますが、表示が崩れるとか使えない状態だったので、気付かなかったら、非常に危ないところでした)
Daily Build版を使用しているので、たまたまデグレが起こったのかも知れませんが、今後「MinimumOSVersion」は必ず設定しようと決めました。

次にAndroid向けに「installLocation="preferExternal"」が設定してあります。
これもおかしな現象なのですが、デバッグ・ビルドにおいては、端末にインストールしたアプリはSDカードに移動可能でしたので、デフォルトでそうゆう設定になっているのだろうと思っていたのですが、いざリリースしてみると、SDカードに移動できません。なんでやねん!ってことで、すぐさま修正版をリリースすることに。
※アプリの動作には関係ないですが、アプリのファイル容量が大きいので必ずやっておきたい設定です

まあこんな感じで、Coronaでの初リリースで色々とトラブル?に見舞われました。もしこのトラブルがだれか他の方に起こり「Corona使えねぇよ!クソがっ!」って評価をされていても、しょうがないと思います。実際半分ぐらい私もそう思っています。
※これは評価されるタイミングに用意できてない方が悪いので

あと「versionName="1.1"」ってなってますが、マーケットでの表示は「1.0」のママです。中の人はあまり細かいことには関心がないのかも知れません。

今回は否定的な事も含め、こうゆう現実があることも知っていて欲しいと思い、起こったことを書きました。
しかしながら、Corona SDKが私にとって作りたいと思ったアプリを一番作りやすかった開発環境であることには変わりませんので、この状態が続く限り、Coronaを使っていると思います。

この記事が、同じように躓いたりハマったりすることがあった方に、少しでも役に立てたら嬉しいです。

※私はアンスカのまわし者ではないですし、正直Coronaを広めたいとか考えていません
※人に「ぜひ使うべき」とか「勧めやすい」とは思っていないという意味です

タブレット向けにアプリ作りたいなぁ。

2011/09/14

Director Class 入門 [Corona SDK]

アプリ開発を通していくつか書きたいCorona SDKネタがあるのですが、まずはこの「Director Class」について言及して置く必要があるだろうと。

そこで今回は、LTする気持ちでスライドを作成してみました。


現状Corona SDKで実際にアプリをリリースしようと思ったときに必要な情報が無さ過ぎるので、今後もできるだけ情報発信していきたいと思っています。

2011/08/31

Corona SDKでiPhone/Androidアプリをリリース

【予告】Corona SDKでiPhone/Androidアプリをリリース(予定)で予告したアプリをリリースしました。

アプリ紹介サイト
iTunesストア
Androidマーケット




スクリーンショットを見ていただければ分かるように、女性をターゲットにしたアプリです。
※動画はiPhoneで見せていますが、Androidでも全く同じUI(デザイン)です

このアプリをひと言で表すと「ピンク色の端末に似合うアプリ」です。

Corona SDKでアプリ開発をしようと思った経緯は前回書いた通りなのですが、実際にアプリのアイデアを検討している際に、以下の点から、本アプリを開発することを決定しました。

・無料の星占いAPIを見つけた点
(試してもらうために無料アプリとしてリリースしたかったので)

・カスタマイズ度の高いアプリとして女性向けにデザインされたアプリというのが合致している点
(女性向けというのはある意味分かりやすいかな?いや難しいか?)

・同類アプリが数多くリリースされている中で、デザインの差別化がどれだけ通用するか見たかった点
(本当にデザインは重要なのか? また、通用するデザインをつくることができるのか?)

・自分自身あまりやらないテイストのデザインに挑戦したかった点
(個人的なテイストとはかけはなれているので、デザインには通常よりも時間がかかりました。しかしながら、デザイナとして要件を満たすデザインは出来なくては!また出来るからこそデザイナです)

Corona SDKで開発したアプリ、しかもゲームではなくツール系で、なおかつ上記のような意味合いも含んだテストケースアプリとして、どこまでの結果が残せるか期待も込めて、しばらくは見守っていきたいと思っています。
※もちろんメンテナンス的アップデートや、プロモーション的なことはやっていきますよ!

ぜひダウンロードしてみてください m(_ _)m

Cutie Horoscope - keygx
Cutie Horoscope - keygx

Cross-platform Mobile App Development Showcase


2011/08/26

Corona SDKでSqlite3を使う時の簡単なまとめ

Corona SDKでSqlite3を使ったアプリ開発をした際に躓いたりしたので、簡単にまとめておこうと思います。

アプリ内ですからそんなに複雑なSQL文はありません。しかし、SQLの種類によって関数の使い分けが必要でした。最初そこが分からずに意図した値が返ってこないことに悩みました。
そう、値が返ってくるかどうかが、ポイントだったのです。

下記のサンプルコードを見ていただくと分かるのですが、簡単にいうと、SELECT文の時は db:row() や db:nrows() を使い、CREATE文やINSERT文の時は db:exec() を使います。
※くわしくはこちら

ドキュメントの読解力が足りず、COUNT関数の実行に db:exec() を使い、ずっと0しか返って来ない… ってことを延々やっていたわけです(^_^;)
お恥ずかしい
-- Include
require("sqlite3")

-- DB SETUP
local path = system.pathForFile("test.db", system.DocumentsDirectory)
db = sqlite3.open(path)
print("DB PATH: "..path)

local tableName = "User"
print("TABLE NAME: "..tableName)

-- Create Table
db:exec([[CREATE TABLE IF NOT EXISTS ]]..tableName..[[ (id INTEGER PRIMARY KEY, name, age); ]])

-- Insert Data
local name = "hoge" 
local age = 20
db:exec([[INSERT INTO ]]..tableName..[[ VALUES (NULL, ']]..name..[[',']]..age..[['); ]])

-- Count
for row in db:rows([[SELECT COUNT(*) FROM ]]..tableName..[[; ]]) do
  print("ROWS: "..row[1])
end

-- Select Data
for row in db:nrows("SELECT * FROM "..tableName) do
  print("NAME: "..row.name)
  print("AGE: "..row.age)
end

-- Update
age = 30
db:exec([[UPDATE ]]..tableName..[[ SET age=]]..age)
for row in db:nrows("SELECT * FROM "..tableName) do
  print("NAME: "..row.name)
  print("AGE: "..row.age)
end

-- Delete
age = 40
db:exec([[DELETE FROM ]]..tableName..[[ WHERE age < ']]..age..[['; ]])
for row in db:rows([[SELECT COUNT(*) FROM ]]..tableName..[[; ]]) do
  print("ROWS: "..row[1])
end

--
実行結果はこうなります。
※まるごとmain.luaとしてコピペで実行できるように書きました。途中ageの代入式が度々出てくるのはその為です
※上記コードには記述していませんが、適切にDBのCLOSE処理が必要です
Corona SDK≫Docs≫Data and Files≫Database (SQLite)
 
最後にTipsとして、Corona SimulatorのFile > Show Project Sandbox というメニューを選択すると、ローカルに保存されている.dbファイルの場所を開いてくれます。 この.dbファイルをLita等のGUIツールで開けば、データをGUIで確認したり、SQL文のテスト等に使えて大変便利でした。

2011/08/25

【予告】Corona SDKでiPhone/Androidアプリをリリース(予定)

ここ2ヶ月ほど、Corona SDKでのアプリ開発を試行錯誤しつつも進めており、やっとアプリをAppleに申請中というステータスになりました。
※Androidは審査がないので、いつでもリリース可ですw

そもそもCorona SDKでアプリを出そうと思ったきっかけは、まぁ気に入ったからという一言で片付けてしまってもいいのですが、あえて言うと「UIが自由に作れるということは、同じUIでiPhoneとAndroidアプリがつくれそう」という理由からです。

Corona SDKは一般的には2Dゲームに向いたプラットフォームだと思われています。これには全く以て同意なのですが、アプリの画面(シーン)を管理するDirector Classというライブラリを使った時に「普通のアプリと同じような画面遷移が作れるんだ!」ということは「普通のアプリっぽい画面を作れば、ゲームじゃないアプリも作れるよね」「だったらiPhone/Android共通のUIを作って、ワンソースで両対応のアプリを作ってみたい」となったわけです。
※最近、iPhone/Androidそれぞれの標準UIを見飽きてきた感があったのと、もうそろそろ、デザインによるアプリの差別化が本格化すると思ってます

まずは、タイトルバー、タブ、TableView(ListView)などをオリジナルでデザインしたUIのプロトタイプを作成してみました。
このプロトタイプで手応えを感じたので、リリースを目的とした(そしてCoronaのライセンスを購入する!)アプリ開発に取りかかることにしました。

色々な思惑もあり(カッコつけるとマーケティング的理由により)リリース向けのアプリは、プロトタイプで作ったデザインとは全く別物になりましたが、なかなかに良い出来です。プロトタイプで作ったUIも良いデザインなので、そのうち別のアプリで使おうと思ってます。無駄にはしません キリッ

今は、アップルに「そのまま審査通してくれ〜」と願いつつ、通知を待っています。
※リジェクトされたらされたでノウハウにはなるのですが…

『技術的に可能でもアップル的にはNGということは多くある』
という残念な結果にならないように

結果は後日!

2011/06/09

CoronaSDKでゲームっぽいものを作ってみた

ATNDで募集されていた【CoronaSDK】でスマートフォンアプリを作ろう!(全6回講座・デジハリ渋谷)に参加しました。

タイトルにある通り全6回の講座で、講師は「日本コロナの会」の代表 山本さんがつとめられていて、講座を通してアプリの完成(最後に発表)を目指します。こんな講座が無料とあっては参加するしかないと速攻申し込みをして、全6回を無事終了しました。(講師の山本さん、日本coronaの会の皆様、企画・運営されたHatchupの皆様、ありがとうございました)

以前のエントリにも書いている通り、私はJavaでのAndroidアプリ開発の他、Titanium MobileやAIR for Androidでもアプリ開発経験があり、以前からCoronaの名前は聞いていたものの、さわるのは今回が初めて「見せてもらおうかCoronaの性能とやらを」と興味津々でCoronaSDKの開発にチャレンジした次第です。

どんなものを作ったかは、発表のスライドとアプリの動画をご覧ください。



※Corona Simulatorで実行


スライドにも書いてありますが、CoronaSDK+Luaでの開発はコードが書きやすくて結構楽しかったです。またTitanium Mobileと比較した場合、直接的にかぶっている部分は少なく、使い分け(棲み分け)ができそう。と個人的には思っています。
※アプリによってCorona向き、Titanium向きがあると思います。例えば「ぐぐっと!急上昇ワード」のようなアプリは完全にTitanium向きなアプリです。

今回CoronaSDKで、初めて「ゲームっぽいもの」を作ることができました。ライトなゲームや自由なUIのアプリ開発にとても向いていると思います。

引き続きCoronaSDKをウォッチしていきたいと思います。


追記:上記アプリのStoryboard API版のソースを公開しました

2011/06/01

Titanium MobileでのTiwitter OAuth認証

先日リリースしたiPhone版「ぐぐっと!急上昇ワード」Ver.1.3.0にツイート機能を付けた時のメモ。

ご存じのようにTitaniumはJSです。JSでOAuth認証ってどうやるの?と思ってましたが、いつも参考にさせていただいているTitanium Mobileで作る! iPhone/Androidアプリ「第4回 TitaniumでTwitterクライアント──OAuthを使ったAPI呼び出し」で取り上げられていますので、これを参考にすれば簡単にできるんじゃねぇと思っていたのですが、「エラー」で動きませんでした。

というわけでいつものように頼れるGoogle先生で調べました。

「OAuth認証してツイートしてタイムラインを取得できるiPhoneアプリを10分で」
すげーっす。動画で分かり易い。そして、ライブラリまで作られているそうなので、拝借させていただきました。

mogya / tm_twitter_api
https://github.com/mogya/tm_twitter_api

しかし、エラーが出ます。
さらにググりました。

「Titanium MobileでTwitterOAuth時のエラーと解決方法」
先の動画とこちらのページの解説で理解できました。
OAuth認証で発行された「PINコード」をスクレイピングして自動入力させている処理が、認証画面が変更になったのでエラーになっていたと。(gihyo.jpさんのコードも同じエラーだったようで…)
mogyaさんのtm_twitter_apiもoauth_adapter.jsを使っているので、上記サイトを参考に該当箇所を修正しました。
追記:修正版がコミットされたようです(2011/06/04)

動いた!ヤター

ふぅ、しかし認証画面が変更される度に修正が必要になるかもですねぇ。

実際のアプリでは以下のような感じになります。

ツイート画面に遷移すると、初回に認証が求められます

IDとパスワードを入力して[アプリを認証]ボタンをタップ PINコードが表示されます。数秒後自動で消えます(この時裏で処理してる訳ですね)

認証完了しました 認証したアカウントのアイコンも表示するようにしました

※初回の認証を呼ぶタイミングは実装次第です。アプリの流れ的に今回は画面に遷移した初回のタイミングにしています。
※アイコンの表示は認証とは別処理です。
※iPhoneでしか試していませんのでAndroidでの動作は不明です。

そのうちTwitter以外のサービスでもOAuth認証を試してみたいと思います。


※サンプルコードをお探しの方には下記の書籍をおすすめします

2011/05/16

Androidアプリ公開から1年がたちました

Androidアプリ「ぐぐっと!急上昇ワード」をリリースしてから1年が経過しました。

1年間で7万を越えるダウンロードをいただき、とても嬉しく思っています。
ユーザーの皆様、ありがとうございます。

現在も日々ダウンロードされていますので、今年中に10万ダウンロードを越えるように、頑張っていきたいと思っています。(できるだけ要望に応えられるように改善していきたいと思っています)
広告のインプレッションも月間30万を越えました。こちらの数字からも、多くのユーザーの方々に使っていただいている実感を味わっております。

引き続き、よろしくお願い申し上げます。

2011/04/15

Titanium Mobileで開発したiPhoneアプリをリリース(その2)

前回に引き続き、今回はTitanium Mobileで開発している時に調べたこと、教えていただいたこと等をご紹介したいと思います。

まずアプリの基本的な部分はgihyo.jpの連載を参考にさせていただきました。UIがツイッタークライアントと似ている部分も多く、大変参考になりました。

その他に開発過程で挙がった課題について書いて行きます。
日本語のアプリ名
ImageViewの画像リサイズとデフォルト設定
Safariで開く
admobを仕込む
ビルド


日本語のアプリ名
Titanium Developerでプロジェクトを作成する際にアプリ名を英語で付けていましたが、最終的にはAndroid版と同じ「ぐぐっと!急上昇ワード」としてリリースしたいと思いました。

titanium-mobile-doc-ja
iOS系の場合はbuildフォルダに生成されたInfo.plistを加工後、プロジェクト直下に配置しておけばOK。
との記述が

Info.plist(一部抜粋)

 CFBundleDevelopmentRegion
 Japanese
 CFBundleDisplayName
 ぐぐっと!
 ...

1行に入るように「ぐぐっと!」だけ入れてますが、これで日本語アプリ名が表示されるようになりました。

また、Titanium BBS(JP unofficial)によるとAndroidでも可能なようです。



ImageViewの画像リサイズとデフォルト設定
キーワードに合わせてサムネール画像を取得している関係で、都度画像サイズが異なります。
そこで画像取得後リサイズしたいのですが、その方法とデフォルト画像を設定するとさらに良いのではと思います。

app.js (一部抜粋)
var thumb_img = Ti.UI.createImageView({
  defaultImage:'./images/default_image.png', //デフォルト画像を指定
  image:encodeURI(画像ファイルのURL),
 width:'auto',
 height:'auto'
});
// 画像読み込み完了時のイベント
thumb_img.addEventListener('load', function(e){
  // height 170以上の場合縮小
  if(.height>170){
    thumb_img.width = thumb_img.width*(170/thumb_img.height);
    thumb_img.height = 170;
  }
});
view.add(thumb_img);
読み込み完了後にリサイズ処理が走りますので、画面表示後、縮小された画像が表示されるまで多少タイムラグがあったりします。
なので実際に表示したい画像が表示されるまでのつなぎ、および最終形に近いサイズでの領域確保の意味合いもあります。
また、デフォルト画像を指定すると、画像の取得に失敗してもデフォルト画像が表示されているので、ごまかせたりもします。
今回のアプリでは画像があるというのがわかるようなデザインにしていますが、背景色と同じ画像を使ってもいいと思います。



Safariで開く
アプリ内でWebViewを使ってWebページを表示させ、ユーザーの任意の操作によってSafariを開きページを引き継ごうとした際に、すでにページ遷移している場合も考えて、現在表示されているページのURLを取得する必要があります。

app.js (一部抜粋)
// WebViewで現在表示しているページのURLを取得
webView.addEventListener('load', function(e){
  currentUrl = e.url;
});
// Safariで開く
btn_safari.addEventListener('click', function(){
  Titanium.Platform.openURL(currentUrl); //Safariで開く
});
WebView内でページがロードされるたびに、URLを取得しておきます。そいつを渡してやればOK!



admobを仕込む
Titaniumの開発元であるAppceleratorよりadmobモジュールが公開されたことで、Titanium Mobileで開発したアプリにも広告を掲載することが可能になりました。
モジュールの使い方がよくわからないので、Twitter経由で質問したところ、親切にもブログで解説していただいたので、参考にさせていただきました。

AdMobモジュールの設置方法

ダウンロードしてきたadmobモジュールを解凍しると、そのまま使える状態(ビルド済み)だったので、指定されたディレクトリにそのままコピーしました。
/Library/Application Support/Titanium/modules/iphone/ti.admob/......
↑こんな感じのパスになるように
※私が組み込んだ時点ではビルド不要だったのですが、今はビルドが必要との情報もあり

あとは、指定されたコードを追加します。
app.js (一部抜粋)
// admob
Titanium.Admob = require('ti.admob');
var adview = Titanium.Admob.createView({
  bottom:0,
  left:0,
 width:320,
  height:48,
 publisherId:'admobのパブリッシャーID'
});
win.add(adview);

tiapp.xml(一部抜粋)

  ...
 
    ti.admob
 

tiapp.xmlでは、最後あたりに追加すれば良いみたいです。

これで広告が表示されると思います。もうモジュールも怖くありません。

しかしadmobモジュールでハマった点がありました。何故か一番上(最初)のWindowでないと広告がタップできないのです。
アプリの作りが[リスト>詳細]となっていて、最初その詳細画面に設置していました。そうすると、広告は表示されるのですがタップできないという現象が…
回避方法が見つけられなかったので、一番上 のWindowにadmobを移動させました。何が悪いの?



ビルド
Titaniumでビルドするととても時間が掛かるようです。(私は途中でキャンセルしました)Titaniumで書き出されたXcodeのプロジェクトファイルをダブルクリックしてXcodeを起動してビルドした方が早いですし、実機転送やオーガナイザでの画面キャプチャなど、その後の作業がスムースに運んで幸せになれると思います。

ビルドやテスト、リリースについては、ネイティブ(というか通常のiPhone開発ですねw)での情報が入手しやすいです。
開発効率でTitanium mobileを選択したならば、いいとこ取りでいきましょー


開発中は必須のページ
下記は特にお世話になりました。
titanium-mobile-doc-ja
API Reference Guide
上記に書いてないことは、“titanium ○○○ ×××”見たいな感じでググって、根気よく探しています。
今後は、Titanium BBS辺りに集約されていくのかな?


以上、大したことない内容ですが、誰かのお役に立てば嬉しいです。


「ぐぐっと!急上昇ワード 」
ぐぐっと!急上昇ワード - keygx

2011/04/13

Titanium Mobileで開発したiPhoneアプリをリリース(その1)

リリース後2週間以上経ってしまいましたが、iPhoneアプリをリリースしたので、少々書き記しておきたいと思います。

アプリ名:ぐぐっと!急上昇ワード
価格:無料アプリ
動作環境:iOS3.1以降
リリース:3/27
対象地域:日本


ぐぐっと!急上昇ワード - keygx

去年リリースしたAndroidアプリ「ぐぐっと!急上昇ワード」が順調にDL数を伸ばし、「来月には5万DL突破だな」といった2月中旬「無料のAndroidアプリ(途中から広告入り)を開発&リリースし、運用してみて色々勉強になったけど、ここらで一段落かな。」と感じるようになり、「iPhoneアプリをやるしかない!」とiPhoneアプリ開発を決意しました。

実はTitanium Mobileを昨年末ぐらいにいじっていて、「ぐぐっと...」を途中まで作っていたのですが投げ出していて、今回iPhoneアプリを開発するにあたってはObjective-C(俺のC言語のスキルはさわり程度しかない)でやる!と意気込み、入門書を購入し、サンプルコードを参考に「ぐぐっと...」iPhone版を作り始めました。
ある程度まで進んだところで(偶然にもTitaniumで投げ出したのと同じようなところまでだった(^_^;))さて次はどうやるんだろうと試行錯誤モードの時に、気まぐれでTitaniumではどうやるんだろうとやってみると。。。アレ?なんか続きが出来たぞ!「このままTitanium Mobileで開発しよう」と、当初の意気込みは何処へやら、Titanium MobileのiPhoneアプリと相成ったという訳です。(Titaniumをいじろうと思ったのは、hidev勉強会でTitaniumやってる人が意外に多かったてのもあります)

その後はいくつかつまずくところがあったものの、比較的スムースに完成したのではないかと思います。
機能的にはAndroid版と同じものを目指しているので、粛々と実装をしていくだけだったのですが、INTENTでブラウザに処理を投げる&Backキーで戻るというAndroid的な処理ができないので、iPhone版はまずアプリ内ブラウザ(WebView)で見せて、Safariに引き継げるようにしたり、ナビゲーションをどのように実現するか(戻るキーとメニューキーの偉大さを実感)を考えないといけなかったりしたところが最大の課題だったともいえます。
※Titanium Mobileの開発で調べたことやTwitterで教えてもらったことは別途書こうと思います。

Developer登録でよくある問題に陥ったり、Reviewに時間がかかったり、といったことも経験しましたが、ググれば山のように出てくる話なので、ここでは割愛します。

余談ですが、Android版「ぐぐっと...」をリリースした時は、このジャンル(Google急上昇ワードのマッシュアップ)のアプリはiPhoneアプリの「トレ○ドなう」ぐらいだったと記憶していて、「ぐぐっと」はAndroidでは少なくとも先行者であると自負しています。今回iPhoneアプリを調べてみると、いくつかの類似アプリがリリースされていて、ひとつのジャンルが形成されていると感じました。(つまり、ニーズがあるということ)

iTunes Connectの見方がイマイチ分かりづらく、数字を読み間違えていないならば、リリース後2週間程で約5,000DLされた模様です。
この数字が多いのか少ないのかさえ分からない位、iPhoneアプリのノウハウを持ってないのですが、元々Long Termを狙ったアプリなので、今はあまり気にしないことにします。

今後もアプリの小さいアップデートを計画していますが、Android版も含めアプリとしての基本形は出来ていると思っています。
このアプリというかこのサービスを進化させていくためには、今後別のサービスとの連携が必要かなと思っていて、今それについて考えているところです。

とりあえず、技術的な話のない第一弾ということで。

次回に続く

ぐぐっと!急上昇ワード - keygx

2011/01/19

TwitterとGoogle App Engineの自分用Webアプリケーション「TwitMail」(ソース)

先月つくったTwitMail(TwitterとGoogle App Engineの自分用Webアプリケーション)、未だTaskQueueの実装が済んでいないですが、現状のもののソースを公開しておきます。

app.yaml
application: アプリ名
version: 1

runtime: python
api_version: 1

handlers:
- url: /main
  script: get_timeline.py

- url: /send
  script: send_tweet.py

- url: /db/delete
  script: db_delete.py

- url: .*
  script: default.py

cron.yaml
cron:

- description: Get Timeline job
  url: /main
  schedule: every 10 minutes

- description: Send Tweet job
  url: /send
  schedule: every 15 minutes

- description: DB Delete job
  url: /db/delete
  schedule: every 60 minutes

get_timeline.py
# -*- coding: utf-8 -*-
import sys, os, re, urllib, urllib2
import simplejson
import tweepy
from datetime import *
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db


#OAuth
CONSUMER_KEY        = 'CONSUMER_KEY'        #自分で取得した値を入れる
CONSUMER_SECRET     = 'CONSUMER_SECRET'     #自分で取得した値を入れる
ACCESS_TOKEN        = 'ACCESS_TOKEN'        #自分で取得した値を入れる
ACCESS_TOKEN_SECRET = 'ACCESS_TOKEN_SECRET' #自分で取得した値を入れる


#データモデルの定義
class TimelineData(db.Model):
    icon_url   = db.StringProperty()                   #アイコン画像URL
    scr_name   = db.StringProperty()                   #スクリーン名
    usr_name   = db.StringProperty()                   #ユーザ名
    tweet      = db.StringProperty(multiline=True)     #ツイート
    status_id  = db.StringProperty()                   #ステータスID
    reply_id   = db.StringProperty()                   #リプライID
    created_at = db.StringProperty()                   #ツイートされた日時
    ds_created = db.DateTimeProperty()                 #DataStoreに登録された日時
    sent       = db.IntegerProperty()                  #送信済みフラグ
    schema_ver = db.IntegerProperty()                  #スキーマ・バージョン



# メイン処理
class GetTimeline(webapp.RequestHandler):
  def get(self):
    #HomeTimelineの取得    
    auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
    api = tweepy.API(auth)
    home_timeline = api.home_timeline(count=50)
    
    #TimeLineの並び順を逆順に変更
    home_timeline.reverse()
    
    for tw in home_timeline:
      try:
        #同じステータスIDが登録されているかCheck
        registed_check_query = TimelineData().all().order('-ds_created').filter('status_id =', str(tw.id).encode("UTF-8")).get()
        
        print registed_check_query
        
        #登録がなければ、登録
        if None == registed_check_query:
          #データストアに登録
          td = TimelineData()
          td.icon_url   = tw.user.profile_image_url                         #アイコン画像URL
          td.scr_name   = tw.user.screen_name                               #スクリーン名
          td.usr_name   = tw.user.name                                      #ユーザ名
          td.tweet      = tw.text                                           #ツイート
          td.status_id  = str(tw.id).encode("UTF-8")                        #ステータスID
          td.reply_id   = str(tw.in_reply_to_status_id).encode("UTF-8")     #リプライID
          td.created_at = str(tw.created_at).encode("UTF-8")                #ツイートされた日時
          td.ds_created = datetime.utcnow() + timedelta(hours=9)            #DataStoreに登録された日時
          td.sent       = 0                                                 #送信済みフラグ、0は未送信
          td.schema_ver = 2                                                 #スキーマ・バージョン
          td.put()
          
          print "▼Check Status"
          
          print td.icon_url.encode("UTF-8")
          print td.scr_name.encode("UTF-8")
          print td.usr_name.encode("UTF-8")
          print td.tweet.encode("UTF-8")
          print td.status_id.encode("UTF-8")
          print td.reply_id.encode("UTF-8")
          print td.created_at.encode("UTF-8")
          print str(td.ds_created).encode("UTF-8")
          print str(td.sent).encode("UTF-8")
          print str(td.schema_ver).encode("UTF-8")
          
          print "△Save DataStore\n"
          
        else:
          print "△Skip DataStore\n"
          
      except:
        print "△NG DataHandling\n"



application = webapp.WSGIApplication(
                                [
                                  ('/main', GetTimeline),
                                ],
                                debug=True)

def main():
  run_wsgi_app(application)

if __name__ == "__main__":
    main()

send_tweet.py
# -*- coding: utf-8 -*-
import sys, os, re, urllib, urllib2
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db
import base64
from google.appengine.api import mail
from dateutil import parser
from dateutil import relativedelta


#データモデルの定義
class TimelineData(db.Model):
    icon_url   = db.StringProperty()                   #アイコン画像URL
    scr_name   = db.StringProperty()                   #スクリーン名
    usr_name   = db.StringProperty()                   #ユーザ名
    tweet      = db.StringProperty(multiline=True)     #ツイート
    status_id  = db.StringProperty()                   #ステータスID
    reply_id   = db.StringProperty()                   #リプライID
    created_at = db.StringProperty()                   #ツイートされた日時
    ds_created = db.DateTimeProperty()                 #DataStoreに登録された日時
    sent       = db.IntegerProperty()                  #送信済みフラグ
    schema_ver = db.IntegerProperty()                  #スキーマ・バージョン


#メイン処理
class SendTweet(webapp.RequestHandler):
  def get(self):
    
    not_send = TimelineData().all().order('ds_created').filter('sent =', 0).fetch(limit=200)
    #print not_send
    
    count = 0   #ツイート数
    start = ""  #最初のツイート
    body = ""   #ツイートの要約
    #"http://twitter.com/#!/"
    
    if not_send:
      for ns in not_send:
        #時刻を変換
        d = parser.parse(ns.created_at)
        jst = d + relativedelta.relativedelta(hours=+9)
        #print jst
        
        try:
          
          body += '
' + "\n" body += '
' + "\n" body += '
' + ns.scr_name.encode("UTF-8") + ' (' + ns.usr_name.encode("UTF-8") + ')' + '
' + "\n" body += '
' + str(jst).encode("UTF-8") + '
' + "\n" body += '' body += '
' + ns.tweet.encode("UTF-8") + '
' + "\n" body += 'http://mobile.twitter.com/' + ns.scr_name.encode("UTF-8") + '/status/' + ns.status_id.encode("UTF-8") + '' + "\n" if 'None' != ns.reply_id: body += '' body += '
' + "\n" body += '
' + "\n" body += "\n" count += 1 if count == 1 : #start = ns.created_at.encode("UTF-8") start = str(jst).encode("UTF-8") ns.sent = 1 #送信済みフラグの変更 ns.put() print str(ns.sent).encode("UTF-8") except: print count print ns.sent #print body subject = start + " から " + str(count).encode("UTF-8") + "件"; print subject fromAddr = "from: メールアドレス"; toAddr = "to: メールアドレス"; html_h = "\n\n" html_f = "" + "\n" html_body = "" html_body += html_h html_body += body html_body += html_f #print html_body mail.send_mail(fromAddr, toAddr, subject, body=" ", html=html_body) else: print "No Data" application = webapp.WSGIApplication( [ ('/send', SendTweet), ], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()

db_delete.py
# -*- coding: utf-8 -*-
import sys, os, re, urllib, urllib2
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db


#データモデルの定義
class TimelineData(db.Model):
    icon_url   = db.StringProperty()                   #アイコン画像URL
    scr_name   = db.StringProperty()                   #スクリーン名
    usr_name   = db.StringProperty()                   #ユーザ名
    tweet      = db.StringProperty(multiline=True)     #ツイート
    status_id  = db.StringProperty()                   #ステータスID
    reply_id   = db.StringProperty()                   #リプライID
    created_at = db.StringProperty()                   #ツイートされた日時
    ds_created = db.DateTimeProperty()                 #DataStoreに登録された日時
    sent       = db.IntegerProperty()                  #送信済みフラグ
    schema_ver = db.IntegerProperty()                  #スキーマ・バージョン



#メイン処理
class DeleteDS(webapp.RequestHandler):
  def get(self):
    # DataStoreの削除
    deleteCheck = TimelineData.all().filter('sent =', 1).fetch(limit=500)
    
    if deleteCheck :
      for dc in deleteCheck:
        try:
          db.delete(dc)
          print "deleted"
        except:
          print "no delete"
    else:
      print "no delete data"


application = webapp.WSGIApplication(
                                [
                                  ('/db/delete', DeleteDS),
                                ],
                                debug=True)

def main():
  run_wsgi_app(application)

if __name__ == "__main__":
    main()

default.py
print "Content-Type: text/plain"
print ""
print "TwitMail"

まだまだ見直す必要があるコードですが、参考になれば。