markdownで書いてHTMLに変換してBloggerに貼り付けるという苦行に耐えれなくなったのでOctopressに移行します.
新しいBlogは http://pchw.github.com です.
とりあえずRubyMotion系の記事は移行しときました.
^p^ http://twitter.com/pchw
markdownで書いてHTMLに変換してBloggerに貼り付けるという苦行に耐えれなくなったのでOctopressに移行します.
新しいBlogは http://pchw.github.com です.
とりあえずRubyMotion系の記事は移行しときました.
今日はRubyMotionでGCD(Grand Central Dispatch)を使う話です.
GCDというのは,
非常に効率的なシステム機能と使い勝手のよいプログラミングモデルを併用して,マルチプロセッサを最大限に活用するために必要なコードを徹底的に簡素化
するものらしいです (AppleのGrand Central Dispatchの説明より)
UITableViewとかそういうのはMainThreadでちょっと重い処理をすると,すぐにパフォーマンスが悪くなってなんだこのアプリ糞だな!とか言われるので,そういう時には別Threadを立ててMainThreadの処理を邪魔しないように処理を行う必要があります. それを楽にしてくれるのがGCD.
Obj-Cのコードだと,
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// 処理
});
こんな感じになります.
RubyMotionでは,GCD関連はDispatch
クラスを使います.
RubyMotion Runtime Guid にも書いてます.
上で書いたObj-CのコードをRubyMotionで書きなおすと,
Dispatch::Queue.concurrent.async{
# 処理
}
こうなります.超簡単.
さて,実際にはConcurrent Dispatch Queueで時間のかかる処理を行い, 処理を行った結果をUIに反映させることがよくあります. その場合に直接UIへ値を代入とかすると落ちます. UIはMainThreadから更新しなければなりません. その場合は,
Dispatch::Queue.concurrent.async{
# 処理
Dispatch::Queue.main.async{
#UI更新
}
}
このようにDispatch::Queue.main
でMain Dispatch Queueに更新処理を入れることで実現できます.
実際に少し書いてみます.
$ motion create gcd
$ cd gcd
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame UIScreen.mainScreen.bounds
@window.rootViewController = NSBundle.mainBundle.loadNibNamed(
'RootViewController',
owner: self,
options: nil).first
@window.rootViewController.wantsFullScreenLayout = true
@window.makeKeyAndVisible
true
end
end
resouces/RootViewController.xib
という形でUIButton(Tag 2),UILabel(Tag 1)を追加しておいて下さい.
(参考:[RUBYMOTION] INTERFACEBUILDERと合わせて使って楽をしよう)
class RootViewController < UIViewController
def viewDidLoad
@label = view.viewWithTag 1
@button = view.viewWithTag 2
@button.addTarget(
self,
action: 'onClicked:',
forControlEvents:UIControlEventTouchUpInside)
end
def onClicked(sender)
p "onClicked"
Dispatch::Queue.concurrent.async {
NSThread.sleepForTimeInterval 5
Dispatch::Queue.main.async {
@label.text = ["hoge", "fuga", "moge"].sample
}
}
end
end
$ rake
シミュレータが立ち上がり,Buttonを押すとLabelの文字列がちょっと待ったあとに変わります. 連打すると次々変わると思います. その間Buttonが押せないとかそういうことが起こらないのが分かると思います.
RubyMotionが1.4にアップデートされてます.
$ sudo motion update
しましょう.
変更履歴は↓
= RubyMotion 1.4 =
* Added support for the compilation of .xcdatamodeld and .storyboard resource
files. Thanks Ian Phillips, Andrew Vega and Michail Pishchagin.
* Fixed a bug when the build system would fail in case the resources dir does
not exist. Thanks Watson.
* Fixed a bug in the Xcode project vendoring code when header files at
different directory levels would not be properly handled. This also fixes
the motion-cocoapods gem. Thanks Eloy Duran.
* Added a way to start the simulator in retina mode by setting the `retina'
environment variable to `true' (other values will be considered false).
For example: `rake retina=true'. Thanks Marcin Maciukiewicz for the idea.
* Fixed an ABI bug in the way we compile Ruby methods overloading Objective-C
methods returning small C structures that can fit in a 64-bit integer.
Thanks Kristoph Cichocki-Romanov for the report.
* Added support for the iOS 4.3 SDK.
以前書いたエントリ― STORYBOARDを使ってRUBYMOTIONで開発する方法のibtoolで.storyboardを.storyboardcにコンパイルする必要がなくなりますね.
githubとかに上がってるのでresourcesが含まれていないのがありますけど,それをgit cloneしてrakeして失敗するのがなくなりますね. (あれって.gitignoreにresourcesを入れてるんですかね)
XCodeプロジェクト形式で引っぱってくるときにヘッダファイルが他のディレクトリにあった時にハンドリングできなかったのが修正された.
cocos2dでハマってた問題を解決できるかも?あとでやってみよう.
rake retina=true
でretina simulatorが立ち上がるよ
ObjCのメソッドをRuby側でオーバーロードして64bit inttegerに収まる小さいCの構造体を返すときのバグ修正らしい
RubyMotionはBaconというテストフレームワークを使っています.
RSpecとの違いがよく分からない.
$ rake spec
とやると,spec
以下のテストが実行されるっぽいです.
ところが,普通に
$ motion crate redgreen
$ cd redgreen
$ rake spec
とやると,ビルドされてシミュレータが立ち上がりテストが実行されますが,白黒です.
そこで,spec
フォルダで一番初めに読み込まれる .rbの中でカラー表示にさせるスクリプトを実行させれば,カラー表示になるようです.
(そのため, 00***.rb
のような名前にすれば良いようです.)
RedGreenというrake specをカラフルにするライブラリがあるらしいのですが,それをRubyMotion用にアレンジしたものがgithubで公開されています.
を自分のとこに持ってくれば使えます.
先程のmotion create
しただけのやつにコピーして再度テストをすると,このようになります.
カラフルになりましたね.
デフォルトのテストはWindowがあるのを調べるテストなので,実装を書いてGreenにしましょう.
./app/app_delegate.rb
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
true
end
end
テストを再実行します.
$ rake spec
Greenになりました.
./spec/00-redgreen.rb
のstyle = :focused
となっている部分をstyle = :full
とすると長めのログが出ます.
RubyMotionでもStoryBoardが使えるらしいので,試してみた.
$ motion create SB
$ cd SB
おもむろにXCodeを開きます.
File>New>File
でiOS>User Interface>Storyboard
を選びます.
UIViewController設置.
IdentifierをFirst
とか付けます.
UILabelとUIButtonを追加します.
もう一つUIViewController設置.
IdentifierをSecond
とか付けときます.
First
に配置したButtonを,Ctrlを押しながら引っ張ってSecond
のViewControllerへ繋ぎます.
Storyboard Seguesの設定が出るので,Modal
とかを選ぶ.
Cmd+s
でStoryboardをresources/Storyboard.storyboard
とか名前を付けて保存します.
$ ibtool --compile resources/Storyboard.storyboardc resources/Storyboard.storyboard
app/app_delegate.rb
を書く.
Storyboardからロードします.
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@sb = UIStoryboard.storyboardWithName('Storyboard', bundle: nil)
rvc = @sb.instantiateViewControllerWithIdentifier("First")
@window.rootViewController = rvc
@window.rootViewController.wantsFullScreenLayout = true
@window.makeKeyAndVisible
true
end
end
ちなみに,Buttonの動作を変えたい場合などは,First
と付けたUIViewControllerをCustom Classに変更してUIViewControllerを継承したクラスを作成して,prepareForSegue:sender:
で遷移前に処理を行うとか,UIButtonのaddTarget:sender
とかにselectorを登録して処理を行うとかします.
$rake
とすれば,Simulatorが立ち上がります.
設置したButtonを押せばSecondのViewへ切り替わります.
RubyMotionでは外部ライブラリも使えるので,ちょっとゲームでも作ってみようかと思って,ちょうど手元にcocos2d for iPhoneレッスンノートとかがあるのでcocos2dで試してみました.
http://www.cocos2d-iphone.org/download からダウンロード
Stable Versionのcocos2d-iphone-1.0.1.tar.gz を落として解凍する.
$ motion create cocos2dTest
$ cd cocos2dTest
$ mkdir vendor
$ cp -R ~/cocos2d-iphone-1.0.1 ./vendor/cocos2d-iphone
XCodeテンプレートをインストールします.
$ cd ./vendor/cocos2d-iphone
$ sh install-templates.sh -f -u
ひとまず,cocos2dを参考にコードを書きます.
Xcodeを立ちあげてFile>New>Project
をします.
テンプレート選択画面はiOS>cocos2d>cocos2d
を選びます.
AppDelegate.m
とRootViewController.m
とHelloWorldLayer.m
を参考に変換します.
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame UIScreen.mainScreen.bounds
director = CCDirector.sharedDirector
@vc = RootViewController.alloc.initWithNibName nil, bundle:nil
@vc.wantsFullScreenLayout = true
glView = EAGLView.viewWithFrame(
@window.bounds,
pixelFormat:KEAGLColorFormatRGB565,
depthFormat:0)
director.setOpenGLView glView
director.setDeviceOrientation KCCDeviceOrientationPortrait
director.setAnimationInterval 1.0/60
director.setDisplayFPS true
@vc.setView glView
@window.addSubview @vc.view
@window.makeKeyAndVisible
CCTexture2D.setDefaultAlphaPixelFormat KCCTexture2DPixelFormat_RGBA8888
CCDirector.sharedDirector.runWithScene HelloWorldLayer.scene
true
end
def application(application, willResignActive:launchOptions)
CCDirector.sharedDirector.pause
end
def application(application, didBecomeActive:launchOptions)
CCDirector.sharedDirector.resume
end
def application(application, didReceiveMemoryWarning:launchOptions)
CCDirector.sharedDirector.purgeCachedData
end
def application(application, didEnterBackground:launchOptions)
CCDirector.sharedDirector.stopAnimation
end
def application(application, willEnterForeground:launchOptions)
CCDirector.sharedDirector.startAnimation
end
def application(application, willTerminate:launchOptions)
director = CCDirector.sharedDirector.
director.openGLView.removeFromSuperview
@vc.release
@window.release
director.end
end
def application(application, significantTimeChange:launchOptions)
end
end
class RootViewController < UIViewController
end
class HelloWorldLayer < CCLayer
def self.scene
scene = CCScene.node
layer = HelloWorldLayer.node
scene.addChild layer
scene
end
def init
super
@label = CCLabelTTF.labelWithString(
"Hello World",
fontName:"Marker Felt",
fontSize: 64)
size = CCDirector.sharedDirector.winSize
@label.position = [size.width/2, size.height/2]
self.addChild @label
self
end
end
外部ライブラリを使う場合は,Rakefile
の編集が必要.
vendorの設定と,OpenGLESやAVFoundationなどのframeworkの設定や依存ライブラリの設定です.
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'cocos2dTest'
app.vendor_project("vendor/cocos2d-iphone",
:xcode,
:xcodeproj => "cocos2d-ios.xcodeproj",
:target => "cocos2d",
:products => ["libcocos2d.a"],
:headers_dir => "cocos2d")
app.frameworks += [
"OpenGLES",
"OpenAL",
"AVFoundation",
"AudioToolbox",
"QuartzCore"]
app.libs << "/usr/lib/libz.dylib"
app.interface_orientations = [:landscape_right]
end
vendor_project で外部ライブラリのパスを指定し,2つ目の引数を:xcode
にしてxcode projectを指定します.
.aで提供されている場合は,:static
にすると良いです.
本来なら,ここでrake
すればcocos2dがビルドされて,app内がビルドされて無事に動くのですが,
Objective-C stub for message `viewWithFrame:pixelFormat:depthFormat:' type `@@:{CGRect={CGPoint=ff}{CGSize=ff}}@I' not precompiled. Make sure you properly link with the framework or library that defines this message.
というエラーが出てシミュレータが終了してしまいました.
これは,./app/app_delegate.rb
のEAGLView.viewWithFrame
でEAGLView viewWithFrame:pixelFormat:depthFormat:
がRuby側から見えてないようです.
RubyMotionでは,定義を参照するために,gen_bridge_metadata
というのを使って*.bridgesupport
というファイルを作っています.
しかし,Rakefile
で指定した:headers_dir => "cocos2d"
の直下しか見ずにgen_bridge_metadata
をしているようです.
しかし,それだとcocos2d/Platforms/iOS
とかの下の定義が参照出来ずにエラーになっているようです.
(EAGLView.h
はcocos2d/Platforms/iOS
の下にあります.)
そのため,/Library/RubyMotion/lib/motion/project/vendor.rb
を編集するか,自分でcocos2d-iphone.bridgesupport
ファイルを生成する必要があります.
ひとまず,bridgesupport
を生成するスクリプトを走らせることにしました.
def sh(command)
system(command)
end
path = Dir.getwd
source_files = Dir.glob('**/*.{c,m,cpp,cxx,mm,h}')
headers = source_files.select { |p| File.extname(p) == '.h' }
bs_files = []
unless headers.empty?
bs_file = File.basename(path) + '.bridgesupport'
if !File.exist?(bs_file) or headers.any? { |h| File.mtime(h) > File.mtime(bs_file) }
includes = headers.map { |p| "-I" + File.dirname(p) }.uniq.join(' ')
sh "/usr/bin/gen_bridge_metadata --format complete --no-64-bit --cflags \"-I. #{includes}\" #{headers.join(' ')} -o \"#{bs_file}\""
end
bs_files << bs_file
end
そして,改めてrake
します.
同じエラーが出ました.orz.
一応,生成されたbridgesupportを見ても確かに元のものよりは定義が増えているのですが,EAGLView
に関するものが増えておらず,ダメなようです.
cocos2dのForumを見ると,cocos2d-iphone/cocos2d/Platform/iOS
をcocos-2d-iphone
直下に持ってきてrake
するとかアドホックな方法で成功はしているらしいですが,cocos2dのソース側も色々とPath変更をしていたりでめんどくさそうでやりたくないです.
というわけで,まだ問題は解決出来ていないです.
iOS SDK 使ってると,こんな感じのコードがよくあります.
NSHTTPURLResponse *res;
NSError *error;
NSURL *url = [NSURL URLWithString:@"http://google.com"];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSData *data = [NSURLConnection sendSynchronousRequest:req returningResponse:&res error:&error];
最後の[NSURLConnection sendSynchronousRequest: returningResponse: error:]
の部分で,(NSHTTPURLResponse**)
とか(NSError**)
とかを引数に取ります.
この場合は,RubyMotionで書くとこうなります.
err_ptr = Pointer.new(:object)
res_ptr = Pointer.new(:object)
url = NSURL.URLWithString("http://google.com")
req = NSURLRequest.requestWithURL(url)
data = NSURLConnection.sendSynchronousRequest(
req,
returningResponse:res_ptr,
error:err_ptr)
@label.text = res_ptr[0].statusCode.to_s
簡単に言うと,(NSError**)
とかを渡す所になっているメソッドは,Pointerのインスタンスを渡してあげて,error = err_ptr[0]
とかしてデリファレンスして使ってやるといいです.
RubyMotionはいいんだけど,UIをコードで作るのはめんどくさい.
そのため,InterfaceBuilderで作りたい.
RubyMotionの進化が早くて,もう1.3のアップデートが来てます.
このエントリーは1.3にアップデートしてからやってね.
$ sudo motion update
やってみます.まずはひな形作成.
$ motion create IB
$ cd IB
app/app_delegate.rb
を編集します.
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.rootViewController = NSBundle.mainBundle.loadNibNamed(
'MyView',
owner: self,
options: nil).first
@window.rootViewController.wantsFullScreenLayout = true
@window.makeKeyAndVisible
true
end
end
こんな感じで,MyViewっていうNibを読み込んでやることにします.
おもむろにXCodeをたちあげて,(すでに開いている人は開いてるProjectを閉じろ!)File>New>File(Cmd + N)を選んで新規ファイル作成する.
iOSのUser InterfaceのEmptyを選んで,resources/MyView.xib
として保存する.
このままだと,ただの方眼紙なので,オブジェクトを追加していきます.
ObjectLibrary(Cmd+Shift+Alt+3)からViewControllerを追加.
CustomClassの所で,MyViewController
とする.
ObjectLibraryからViewを追加.
ObjectLibraryからButtonとLabelを追加.
この部分が重要で、それぞれのTagに1とか2とかつけます
保存して終了.
次にapp/my_view_controller.rb
を作成します.
class MyViewController < UIViewController
def viewDidLoad
@button = view.viewWithTag 1
@label = view.viewWithTag 2
@button.addTarget(
self,
action:'onClicked',
forControlEvents:UIControlEventTouchUpInside)
end
def onClicked
@label.text = "Clicked"
end
end
1のTagを付けたのをbuttonとして,クリックのイベントをonClickedメソッドに登録.
2のTagを付けたのをlabelとして,クリックされた時に変更したりするようにしました.
$ rake
でビルドしてシミュレータが立ち上がります.
押します.
変わります!
出来ました!
のBuyから購入.
12,368円ぐらい($199.99からのディスカウントで$149.99)
クレカかPayPalで購入できます.
JCBは使えなかった.JCBはクソ.
購入したらInstallerのDL先とlicense keyがメールで届きます.
InstallerをDLして,起動するとlicense keyを入れるとインストールされます.
を参考にHelloWorldをする.
まずはコマンドの確認.
$which motion
/usr/bin/motion
iOS SDKが入っている必要がありますが,僕の環境には既にXCodeが入っているので飛ばします.
RubyMotionのアップデートが無いか確認.
$motion -v
1.2
$sudo motion update
Password:
Connecting to the server….
Software is up to date
$ motion -v
1.2
最新でした.
Hello Worldします.
$motion create Hello
Create Hello
Create Hello/.gitignore
Create Hello/Rakefile
Create Hello/app
Create Hello/app/app_delegate.rb
Create Hello/resources
Create Hello/spec
Create Hello/spec/main_spec.rb
$cd Hello
$rake
===============================================================================
It appears that you have a version of Xcode installed in /Applications that has
not been set as the default version. It is possible that RubyMotion may be
using old versions of certain tools which could eventually cause issues.
To fix this problem, you can type the following command in the terminal:
$ sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
===============================================================================
Build ./build/iPhoneSimulator-5.0-Development
Compile ./app/app_delegate.rb
Create ./build/iPhoneSimulator-5.0-Development/Hello.app
Link ./build/iPhoneSimulator-5.0-Development/Hello.app/Hello
Create ./build/iPhoneSimulator-5.0-Development/Hello.app/Info.plist
Create ./build/iPhoneSimulator-5.0-Development/Hello.app/PkgInfo
Create ./build/iPhoneSimulator-5.0-Development/Hello.dSYM
Simulate ./build/iPhoneSimulator-5.0-Development/Hello.app
2012-05-09 03:33:40.258 Hello[33099:f803] Applications are expected to have a root view controller at the end of application launch
(main)>>
で,シミュレータが起動しますが,何も書いていないので空です.
./app/app_delegate.rb
を編集します.
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
alert = UIAlertView.alloc.initWithTitle(
"Title",
message:"message",
delegate:nil,
cancelButtonTitle:nil,
otherButtonTitles:"OK")
alert.message = "Hello World"
alert.show
true
end
end
ビルドし直します.
$rake
シミュレータが起動して,Alertが表示されます.
おはようございます!ぽちです。
iPadでプログラミングな記事を書いたら割とポジティブな反応でちょっと意外でした。
「そんなプアーなエディタ環境でやってられるか」みたいな声がもっと聞けるかと思ってました。
さて、別にTextasticの回し者じゃないですけど、またTextasticを使った話です。
iPadでプログラミングするのはいいんですけど、enchant.jsを使ってミニゲームを書いてると同じような初期コードから始めることが多々あります。
Gameオブジェクトの初期化とかonloadとか。
そこでTextasticにはテンプレートっていう機能があります。
ファイル作成時にテンプレートを選ぶと、定型の文が入力された状態でファイルが出来ます。
テンプレートを追加する前に、テンプレートを置いたりするフォルダを作ります。
#Texstastic という名前のフォルダを1番トップの所に作ります。
その#Textstatic フォルダの中にTemplateフォルダを作ります。
この中にテンプレート定義ファイルを入れていきます。
テンプレートは.jsonファイルで、下のような書式で書きます。
{
"uuid": "",
"category": "",
"templates": [
{
"name": "",
"fileExtension": "",
"snippet": ""
}
]
}
実は、この書式はデフォルトのテンプレートに入っているので、ファイル作成時にTemplateを選べば、uuid付きで生成されます。
あと、注意点としてはsnippetの部分は改行は¥nで、インデントは¥tにして書く必要があります。
僕のリポジトリにとりあえず作ったのおいときます。
テンプレートへのリンク
ちなみに、Textastic はURLスキーマが登録されているので、httpとかのところをtextastic:// にすれば、直接取り込まれます。
Templateの.jsonファイルが置けたら、Textasticを終了させて設定を読み込ませます。
再度起動させて、テンプレートが無事読み込まれているか確かめます。
ファイル新規作成して、テンプレートを選ぶと
先ほど追加したテンプレートがみえるはずです!
これで、同じような処理を毎回書く手間が省けて、より効率的にiPadでenchant.js使ってゲームが書けるようになりました!
めでたしめでたし。