iphone-rubycocoaで、実機動作中のアプリをリモートから動的に変更する

先日からの iphone-rubycocoa で一番やりたかったこと、それは、リモートからiPhone実機で動いているアプリを、動的に変更することです。たとえば、画面の一部分のちょっとした色とかの値をいじろうと思っても、ObjC のコードに修正を加えて再コンパイルして実機転送して動作させる、というような時間がかかる (下手すると10秒20秒平気でかかる) ことしないとなりません。例えばそれが、インタラクティブなおかつリアルタイムに変更できたらすばらしと思いません? ということでできるようにしてみました。

この@kuさんの記事によると、同様のことをJSCocoaでもできるようです。が、この時はJSCocoaは、シミュレータでしか動かず実機動作はできませんでした。(今はlibffiがちょっと動いている? そうなのでもしかすると、実機動作するかもしれません)

サンプルを動かすには

最新はここですが、順次更新していっていますので
http://github.com/takuma104/iphone-rubycocoa/tree/master

以下のサンプルを動かしているのはこちらのtagのものです。
http://github.com/takuma104/iphone-rubycocoa/commits/hateda-2009-0302

普通に git clone して、そのあと git co hateda-2009-0302 とかすると良いかもです。そのあとXcodeでbuildとかします。

まず、main.rbの

# require 'remote_irb'
# RemoteIRB.new(6000).start

の部分のコメントを外します。そうすると、iPhoneなどの実機で、TCPの6000 ポートで受けつけます。

注意:とくにiPhoneだと、3Gが使用可能になっていると、globalが空いてたりして、どこからでもアクセスできてしまったりするので、使うときにはセキュリティに十分注意してください。

あとは普通にビルドして実機あるいはシミュレータで動かしてください。

実機上で起動したのを確認したら、telnet

telnet 192.168.4.138 6000

とかすると接続してirbのプロンプトが出ます。*1 (アドレスはiPhoneのアドレスに置き代えてくださいね)。ちなみに readline をサポートしてないので、rlwrap というコマンドを使い (sudo port install rlwrapとかでインストールできます)

rlwrap telnet 192.168.4.138 6000

とかすると履歴とか使えて幸せです。

いろいろ触ってみるサンプル

スクリーンショットで説明すると、

から

にするサンプルです。こんなかんじで色々できます。(適宜コメントを入れました)

# とりあえず動くか確認
irb(main):001:0> 1+2
3

# まずMyAppDeletateのインスタンスを得ようと思う
irb(main):002:0> OSX::UIApplication
OSX::UIApplication

irb(main):003:0> OSX::UIApplication.sharedApplication
#<OSX::UIApplication:0x1eefba class='UIApplication' id=0x26f6d0>

irb(main):004:0> OSX::UIApplication.sharedApplication.delegate
#<MyAppDelegate:0x1f8e70 class='MyAppDelegate' id=0x29dea0>

# 得られたっぽいので、appに保存
irb(main):005:0> app = OSX::UIApplication.sharedApplication.delegate
#<MyAppDelegate:0x1f8e70 class='MyAppDelegate' id=0x29dea0>

# UIWindowのインスタンス
irb(main):006:0> app.window
#<OSX::UIWindow:0x1f8c40 class='UIWindow' id=0x2c0080>

# UILabelのインスタンス
irb(main):008:0> app.textView
#<OSX::UILabel:0x1f8902 class='UILabel' id=0x2e2e00>

# ウインドウの背景色を白にしてみる
irb(main):009:0> app.window.setBackgroundColor OSX::UIColor.whiteColor
nil

# ラベルが見えないので、黒色にしてみる
irb(main):012:0> app.textView.setTextColor OSX::UIColor.blackColor
nil

# なんかさびしいので画像とかネットからとってくるか
irb(main):013:0> gif = open('http://www.ruby-lang.org/images/logo.gif','rb') {|f| f.read }
Errno::ENOENT: No such file or directory - http://www.ruby-lang.org/images/logo.gif
from (irb):13:in 'initialize'
from (irb):13:in 'open'
from (irb):13

# open-uriがないんですねわかります
irb(main):014:0> require 'open-uri'
true

# とれた
irb(main):015:0> gif = open('http://www.ruby-lang.org/images/logo.gif','rb') {|f| f.read }
"GIF89a...."

# 画像データからUIImageをつくる
irb(main):016:0> img = OSX::UIImage.imageWithData(OSX::NSData.dataWithBytes_length(gif, gif.length))
NameError: uninitialized constant OSX::UIImage
from (irb):16

# UIImageって何だよっておこられたので、ns_importする (NameError出た時はこれを試してみてください)
irb(main):017:0> OSX::ns_import :UIImage
OSX::UIImage

# えー、NSDataもないの???
irb(main):018:0> img = OSX::UIImage.imageWithData(OSX::NSData.dataWithBytes_length(gif, gif.length))
NameError: uninitialized constant OSX::NSData
from (irb):18

# はい
irb(main):019:0> OSX::ns_import :NSData
OSX::NSData

# UIImageできた
irb(main):020:0> img = OSX::UIImage.imageWithData(OSX::NSData.dataWithBytes_length(gif, gif.length))
#<OSX::UIImage:0x1e23f0 class='UIImage' id=0x287850>

# UIImageViewをつくってみるよ
irb(main):021:0> imgview = OSX::UIImageView.alloc.initWithImage img
NameError: uninitialized constant OSX::UIImageView
from (irb):21

# またかよ
irb(main):022:0> OSX::ns_import :UIImageView
OSX::UIImageView

# できた
irb(main):023:0> imgview = OSX::UIImageView.alloc.initWithImage img
#<OSX::UIImageView:0x1d1af0 class='UIImageView' id=0x31c48d0>

# とりあえずwindowにaddSubviewしてみる
irb(main):026:0> app.window.addSubview imgview
nil

# なんか上の方にでて微妙なので、frameをいじろうかな
irb(main):028:0> imgview.frame
#<OSX::CGRect:0x376dec @size=#<OSX::CGSize:0x376ba8 @height=119.0, @width=331.0>,
 @origin=#<OSX::CGPoint:0x376d38 @y=0.0, @x=0.0>>

# こんなんでどうだ? (値はあらかじめ試行錯誤してます)
irb(main):030:0> imgview.setFrame [-5,140] + imgview.frame.size.to_a
nil

# テキストの位置も悪いなあ、こんなんでどうだ? (値はあらかじめ試行錯誤してます)
irb(main):034:0> app.textView.setFrame [0,260,320,40]
nil

# テキストをちょっと変えるか
irb(main):035:0> app.textView.setText 'with RubyCocoa'
nil

# フォントもちょっと大きく
irb(main):042:0> app.textView.setFont OSX::UIFont.boldSystemFontOfSize(32)
nil

iphone-rubycocoa プロジェクトの進捗状況とか今後とか

大分前に eval.rb over telnetが動いていたのですが、この irb over telnet はなぜか上手く動かない(特に、例外が発生したりすると、longjmpのあとに、必ず死んでしまうという)謎バグがありました。この原因がようやく分りました。どうやら longjmp() が原因ではなく、そのあと、 sigprocmask() を行った時に落ちていたようです。signal は iPhoneアプリにおいて、そもそも使わないでも良いと思うので(複数プロセスも使わないですし)、ばっさり signal support 自体を削って irb + telnet ができるようになりました。いまのところ安定しているようで、いい感じです。そろそろtest/以下のテストも始めようと思います。

それから、RubyCocoaの方ですが、libffiのほう、どうやらiPhone実機で動いているという話もあり、BridgeSupport相当がちゃんと動いてない現状版をメンテするよりは、最新版(0.13.2)の移植をちゃんとやったほうが良いかなと思いはじめていて、次にこれをやろうと思います。最新版が入れば(BridgeSupportもあるので)もうちょっとまともにRubyCocoaでアプリが書けるようになるのではないかと思います。

あと上のサンプルですが、RubyCocoaで書かれたクラスを動的変更していますが、これは自分でObjCで書いたクラスも同様のはずなので、それをできるようにしてみようかと思います。

*1:なおこちらで何度か試したところ、一番最初のプロンプトがなぜか irb > とかならないという変な現象が出ています。Ctrl-Dとかして一旦切ってもういちど接続するとちゃんと irb > とか出たりします。。