Octopressに移行したのでBloggerは更新しなくなります!


markdownで書いてHTMLに変換してBloggerに貼り付けるという苦行に耐えれなくなったのでOctopressに移行します.

新しいBlogは http://pchw.github.com です.

とりあえずRubyMotion系の記事は移行しときました.

RubyMotionでGCDを使う

GCD

今日はRubyMotionでGCD(Grand Central Dispatch)を使う話です.

GCD とは

GCDというのは,

非常に効率的なシステム機能と使い勝手のよいプログラミングモデルを併用して,マルチプロセッサを最大限に活用するために必要なコードを徹底的に簡素化

するものらしいです (AppleのGrand Central Dispatchの説明より)

UITableViewとかそういうのはMainThreadでちょっと重い処理をすると,すぐにパフォーマンスが悪くなってなんだこのアプリ糞だな!とか言われるので,そういう時には別Threadを立ててMainThreadの処理を邪魔しないように処理を行う必要があります. それを楽にしてくれるのがGCD.

Objective-C

Obj-Cのコードだと,

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    // 処理
});

こんな感じになります.

RubyMotion

RubyMotionでは,GCD関連はDispatchクラスを使います.

RubyMotion Runtime Guid にも書いてます.

上で書いたObj-CのコードをRubyMotionで書きなおすと,

Dispatch::Queue.concurrent.async{
    # 処理
}

こうなります.超簡単.

UIの更新に関して

さて,実際には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

app/app_delegate.rb

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

InterfaceBuilder

resouces/RootViewController.xib という形でUIButton(Tag 2),UILabel(Tag 1)を追加しておいて下さい. (参考:[RUBYMOTION] INTERFACEBUILDERと合わせて使って楽をしよう

app/root_view_controller.rb

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にアップデートしよう!

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.

.xcdatamodeldと.storyboardのコンパイルに対応.

以前書いたエントリ― STORYBOARDを使ってRUBYMOTIONで開発する方法のibtoolで.storyboardを.storyboardcにコンパイルする必要がなくなりますね.

resourcesディレクトリが無くてもrakeに失敗しなくなる

githubとかに上がってるのでresourcesが含まれていないのがありますけど,それをgit cloneしてrakeして失敗するのがなくなりますね. (あれって.gitignoreにresourcesを入れてるんですかね)

他ライブラリ読み込み関連

XCodeプロジェクト形式で引っぱってくるときにヘッダファイルが他のディレクトリにあった時にハンドリングできなかったのが修正された.

cocos2dでハマってた問題を解決できるかも?あとでやってみよう.

retina simulatorサポート

rake retina=trueでretina simulatorが立ち上がるよ

ABIバグ修正

ObjCのメソッドをRuby側でオーバーロードして64bit inttegerに収まる小さいCの構造体を返すときのバグ修正らしい

iOS 4.3 SDKサポート

[RubyMotion] rake specをカラフルにしよう!

RubyMotionはBaconというテストフレームワークを使っています.
RSpecとの違いがよく分からない.



$ rake spec



とやると,spec以下のテストが実行されるっぽいです.



ところが,普通に



$ motion crate redgreen
$ cd redgreen
$ rake spec



とやると,ビルドされてシミュレータが立ち上がりテストが実行されますが,白黒です.



そこで,specフォルダで一番初めに読み込まれる .rbの中でカラー表示にさせるスクリプトを実行させれば,カラー表示になるようです.
(そのため, 00***.rbのような名前にすれば良いようです.)



RedGreenというrake specをカラフルにするライブラリがあるらしいのですが,それをRubyMotion用にアレンジしたものがgithubで公開されています.



rm-redgreen




  • spec/00-redgreen.rb


  • app/app.rb


  • app/kernel.rb


  • app/rm-ansiterm.rb


  • app/string.rb




を自分のとこに持ってくれば使えます.



先程の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.rbstyle = :focusedとなっている部分をstyle = :fullとすると長めのログが出ます.

Storyboardを使ってRubyMotionで開発する方法

RubyMotionでもStoryBoardが使えるらしいので,試してみた.



ひな形作成



$ motion create SB
$ cd SB



XcodeでStoryboardを作成



おもむろにXCodeを開きます.



File>New>FileiOS>User Interface>Storyboardを選びます.



初めに表示されるViewControllerを作成



UIViewController設置.
IdentifierをFirstとか付けます.
UILabelとUIButtonを追加します.



Segueで遷移する先のViewControllerを作成



もう一つUIViewController設置.
IdentifierをSecondとか付けときます.



Segueの設定



Firstに配置したButtonを,Ctrlを押しながら引っ張ってSecondのViewControllerへ繋ぎます.
Storyboard Seguesの設定が出るので,Modalとかを選ぶ.



Storyboardの保存



Cmd+sでStoryboardをresources/Storyboard.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をRubyMotionで使ってみた(迷宮編)

RubyMotionでは外部ライブラリも使えるので,ちょっとゲームでも作ってみようかと思って,ちょうど手元にcocos2d for iPhoneレッスンノートとかがあるのでcocos2dで試してみました.



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



XCodeで作れるテンプレートをRubyMotionで動くようにする



ひとまず,cocos2dを参考にコードを書きます.



Xcodeを立ちあげてFile>New>Projectをします.
テンプレート選択画面はiOS>cocos2d>cocos2dを選びます.



AppDelegate.mRootViewController.mHelloWorldLayer.m を参考に変換します.



./app/app_delegate.rb



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



./app/root_view_controller.rb



class RootViewController < UIViewController
end



./app/hello_world_layer.rb



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の設定



外部ライブラリを使う場合は,Rakefileの編集が必要.



vendorの設定と,OpenGLESやAVFoundationなどのframeworkの設定や依存ライブラリの設定です.



Rakefile



$:.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にすると良いです.



RubyMotionでの詰まりどころ



本来なら,ここで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.rbEAGLView.viewWithFrameEAGLView viewWithFrame:pixelFormat:depthFormat:がRuby側から見えてないようです.



RubyMotionでは,定義を参照するために,gen_bridge_metadataというのを使って*.bridgesupportというファイルを作っています.



しかし,Rakefileで指定した:headers_dir => "cocos2d"の直下しか見ずにgen_bridge_metadataをしているようです.
しかし,それだとcocos2d/Platforms/iOSとかの下の定義が参照出来ずにエラーになっているようです.
EAGLView.hcocos2d/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/iOScocos-2d-iphone直下に持ってきてrakeするとかアドホックな方法で成功はしているらしいですが,cocos2dのソース側も色々とPath変更をしていたりでめんどくさそうでやりたくないです.



というわけで,まだ問題は解決出来ていないです.

[RubyMotion] NSError ** とかを引数に取るメソッドを使う時どうすればいいか

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] InterfaceBuilderと合わせて使って楽をしよう

RubyMotionはいいんだけど,UIをコードで作るのはめんどくさい.



そのため,InterfaceBuilderで作りたい.



RubyMotionをアップデート



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を読み込んでやることにします.



InterfaceBuilderでUI配置



おもむろに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とかつけます

保存して終了.



UIViewController



次に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



でビルドしてシミュレータが立ち上がります.



押します.



変わります!



出来ました!

RubyMotionはじめました

購入

公式サイト

のBuyから購入.
12,368円ぐらい($199.99からのディスカウントで$149.99)

クレカかPayPalで購入できます.
JCBは使えなかった.JCBはクソ.

購入したらInstallerのDL先とlicense keyがメールで届きます.

InstallerをDLして,起動するとlicense keyを入れるとインストールされます.




Getting

GettingStarted

を参考に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が表示されます.