RubyMotion で Storyboard を使う

Storyboard

Objective-CでiOSアプリを開発する際、Storyboard(InterfaceBuilder)を使うとユーザーインターフェイスの設計や画面遷移の設計がやりやすくなります。

コントロールが複雑になったり、表示制御を細かくやる必要があるアプリなんかだと、Storyboard上でのView配置は適当で、アプリケーション側でゴリゴリと位置・サイズ制御することもあったりしますが、使える方が便利なことも少なくありません。

RubyMotionリリース当初はInterfaceBuilderが出力するUI関連のリソースファイルには対応していませんでしたが、 バージョン1.4からStoryboardデータの読み込みに対応しています。

IB以外のUI設計の選択肢も増えてきており、色々楽しみなものもありますが、まぁ基本と言うことでおさらいしてみました。

このエントリでは、StoryboardリソースをRubyMotionアプリケーションに組み込む際に、RubyMotionアプリのクラスをStoryboardのリソースにどう結びつければ良いのか、逆に、Storyboard内のオブジェクトをRubyMotionアプリから操作するにはどうすれば良いのか、あたりを明確にする感じで書いてみたいと思います。

準備 - Storyboardリソースの作成

何はともあれ、InterfaceBuilderでStoryboardリソースを作成しないと始まりません。

Xcodeを起動して、新規ファイル作成でStoryboardを選択します。DeviceTypeはiPhoneでもiPadでもお好きなモノを。

XcodeでStoryboardファイル作成

するとInterfaceBuilderが立ち上がって方眼紙的な編集画面になります。

Storyboardファイルを作成した直後はここに何も表示されていないので一瞬途方に暮れそうになりますが、ウィンドウの右下エリアにツールパレットがあり、ここから色々と作業エリアに追加します。

まずはView Controllerを追加するので、オブジェクトライブラリタブを選択します。

最初のView Controller

今回のサンプルでは、Tab BarとかNavigationとかを使ってみるので、まずはTab Barを追加します。

Tab Bar Controllerを追加

2つのView Controllerを持つTab Bar Controllerが追加されました。

2つのうちの一方をNavigation Controllerに変えたいので、削除した後、オブジェクトライブラリからNavigation Controllerを追加します。

Navigation Controllerを追加

追加したNavigation ControllerをTab Barのタブの1つとしたいので、Tab Bar ControllerとNavigation ControllerをSegueで接続し、Relationを確立します。

Segueは、Controlキーを押しながら、接続元から接続先に向けてマウスポインタをドラッグすると追加できます。

NavigationにSegueを設定

こんな感じになります。

NavigationにSegueが設定された

Navigation Controllerの下位構造としては、Table Viewの下にDetail Viewを持つようにしたいので、View Controllerを追加し、その内部にUIImage Viewを設定しておきます。

また、Table ViewとDetail Viewとの間もSegueで接続する必要があります。
Table ViewのセルをタップしたらDetail Viewに遷移するので、Table Viewのセルを選択した状態で、Segueを追加し、Manual Segue - push にしておきます。

Table ViewにSegueを設定

Navigation Controllerじゃ無い方のタブはとりあえず適当に。

という感じで、こんなサンプルStoryboardを作成しました。

サンプルStoryboard

クラス名とかStoryboard IDとか

ここまでだと、とりあえずお絵描きしただけなので、RubyMotionアプリケーションで扱えるように紐付けを行います。

まず、追加したView Controllerクラスのそれぞれに、RubyMotionアプリ内に定義するUIViewController派生クラスのクラス名を対応付けます。

また、RubyMotionアプリケーションからStoryboardデータにアクセスする際、Stobyboard IDを使う場合があるので、そちらも設定しておきます。

いずれも、InterfaceBuilderウィンドウの右上エリアでIdentity Inspectorタブを選択することで設定UIが表示されます。

Initial View ControllerにStoryboard IDを設定

ここまでできたらファイルを保存。

コンパイル

そうやって作成したStoryboardファイルはXML形式のファイルで、そのままではRubyMotionアプリケーションでは利用できません。

ibtoolを使ってコンパイルする必要があります。

$ ibtool --compile StoryboardExample.storyboardc StoryboardExample.storyboard

そうやってできたstoryboardcファイルを、RubyMotionアプリケーションのresourcesディレクトリに配置します。

Storyboard Fileをコンパイル

これで準備完了です。

とりあえず実行してみる

リソースも組み込んだので、とりあえず動かしてみたい衝動に駆られます。

しかし、そのままビルドしてもうまくいきません。

AppDelegateのapplicationメソッドにStoryboardリソースの読み込み処理を実装する必要があります。

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)

    @storyboard = UIStoryboard.storyboardWithName('StoryboardExample', bundle:nil)

    @window.rootViewController = @storyboard.instantiateViewControllerWithIdentifier('TopTab')
    @window.rootViewController.wantsFullScreenLayout = true
    @window.makeKeyAndVisible

    true
  end
end

UIStoryboard.storyboardWithNameの引数はファイル名のベース部分、
@storyboard.instantiateViewControllerWithIdentifierの引数は、Initial View Controllerに設定したStoryboard IDです。

とりあえずここまでやってビルド&実行すると、こんな感じになり、リソースが正しくロードされていることが分かります。

とりあえず実行してみる

RubyMotion側のView Controllerクラスに正しくマッピングされているのかも気になりますので、それぞれのクラスのviewDidLoadメソッドをオーバーライドして、ログとか吐くようにしても良いかも知れません。

class TopTabController < UITabBarController
  def viewDidLoad
    puts 'top tab loaded'
  end
end

class MainNavController < UINavigationController
  def viewDidLoad
    puts 'main nav loaded'
  end
end

コンテナタイプのView ControllerであるTab Bar ControllerやNavigation Controllerの場合、下位のView Controllerもあわせてインスタンス化されていることが分かります。

View Controllerインスタンス化

今回のサンプルアプリ

今回は、RubyMine Enokiの紹介エントリを書いた際に使った、楽天APIのサンプルを流用します。

テーブルビューのセルをタップしたら詳細画面に遷移して、画像を表示する、というような動作にしたいと思います。

ソースコードはこちら。
サンプルコード

RubyMotion内のビューコントローラで制御

さて、ここまでで自作ビューコントローラの実体化&紐付けまではできています。

先ほど使ったviewDidLoadなど、各種のイベントフック等でクラス内に制御が渡されるので、これだけでも色々できるようになっています。

例えば、今回のサンプルに含まれている一覧画面に対応するUITableViewController派生クラスでは、こんな感じで通常通りセルの描画処理なんかを書く訳ですね。

def tableView(tableView, cellForRowAtIndexPath:indexPath)
  cell = tableView.dequeueReusableCellWithIdentifier(CELL_REUSE_IDENTIFIER) || begin
    UITableViewCell.alloc.initWithStyle(
      UITableViewCellStyleDefault,
      reuseIdentifier:CELL_REUSE_IDENTIFIER
    )
  end

  item = @data[indexPath.row]

  # cell style
  cell.accessoryType  = UITableViewCellAccessoryDisclosureIndicator

  # text label
  image_data = NSData.dataWithContentsOfURL(NSURL.URLWithString(item[:image_url]))
  image_view = UIImageView.alloc.initWithImage(UIImage.imageWithData(image_data))
  image_view.frame = CGRectMake(5, 5, 32, 32)
  cell.addSubview(image_view)

  # thumbnail
  label = UILabel.alloc.initWithFrame(CGRectMake(45, 10, 250, 20))
  label.font = UIFont.boldSystemFontOfSize(11)
  label.text = item[:name]
  cell.addSubview(label)

  return cell
end

実行するとこんな感じでいつもの安心感。

サンプルアプリ一覧画面

Segueを利用した画面遷移の制御

次に、テーブルのセルをタップした際に、詳細表示画面に遷移できるようにしなければなりません。

Storyboard上でSegueを設定していても、それだけでは画面遷移されません。
Segueを駆動させるコードを書く必要があります。

tableView:didSelectRowAtIndexPathをオーバーライドし、
performSegueWithIdentifierをコールします。

def tableView(tableView, didSelectRowAtIndexPath:indexPath)
  performSegueWithIdentifier('to_detail', sender:self)
end

このとき、実行するSegueをIDで指定する必要がありので、InterfaceBuilderで設定しておきます。(storyboardファイルを変更したら再度コンパイルを忘れずに)

これでセルのタップでSegueが実行されるようになりました。

今回のサンプルアプリでは、詳細画面内に商品画像を表示しますので、Segueで遷移する際、表示すべき画像のURLを詳細画面に教えてやる必要があります。

このために、UITableViewController派生クラスでprepareForSegueメソッドをオーバーライドし、下記のような処理を実装します。

def prepareForSegue(segue, sender:sender)
  item = @data[@table.indexPathForSelectedRow.row]

  detailViewController = segue.destinationViewController
  detailViewController.image_url = item[:image_url]
end

segue.destinationViewControllerで遷移先のView Controllerインスタンスを取得できるので、それに対してURLの設定メソッドをコールしています。
(もちろん、受け側のView Controllerクラスにメソッドを実装する必要がある)

RubyMotionでビューオブジェクトを操作

ようやく詳細画面までたどり着きました。

詳細画面のコントローラ(DetailController)の実体化もできて、表示すべきURLも受け取っています。
では、画像を表示すべき対象のUIImageViewをどうやって取得すれば良いのでしょうか?

まぁ、viewDidLoadあたりでUIImageViewのインスタンスを作れば良いのですが、
今回のテーマ的には、Storyboard上に設定したViewをRubyMotionアプリから参照したいところです。

これにはいくつか方法があります。
Xcode側でダミーのOutletを設定してRubyMotion側のattrと結びつけるとか、Nitronを使うとか。

今回は、とりあえずお手軽なView Tag参照を使います。

Interface Builderで、参照したいViewにTagを付けておきます。

ViewにTagを付ける

そして、View Controller側で以下のようにすると、タグ付けしたViewのインスタンスを取得して操作できます。

def viewDidLoad
  image_data = NSData.dataWithContentsOfURL(NSURL.URLWithString(image_url))

  @image = view.viewWithTag(1)
  @image.initWithImage(UIImage.imageWithData(image_data))
end

てな感じで画像を表示できるようになりました。

サンプルアプリ詳細画面

サムネイルの画像をそのまま表示しているので、非常に残念な出来映えとなっております。

まとめ

今回は、

  • View Controllerの対応付け
  • Segueによる画面遷移の制御
  • Viewの参照

あたりを見てきました。

Storyboardを使うことでユーザーインターフェイスの開発は容易になるケースも結構あると思うので、活用していきたいですね。

RubyMine Enokiを使ったRubyMotion開発

RubyMine Enoki

RubyでiOSアプリを開発できるRubyMotionですが、とうとう、JetBrainsのRuby統合開発環境であるRubyMineのRubyMotion対応バージョンである「RubyMine Enoki」が使えるようになりました。

まだ正式リリースは少し先のようですが、EAP(Early Access Program)というベータ版・リリース候補版利用プログラムで一足先に使えるようになっています。

RubyMine Enoki Early Access: RubyMotion is on Board

RubyMineの現行バージョンは4.5で、Enokiはver.5のコードネームっぽいですが、スプラッシュスクリーンに「榎」(そう、あのえのきでございますよ)の透かしが大きく入っており、もっと引っ張るんじゃないかという期待感が高まります。

RubyMine Enoki

えのきの特徴

Enokiはメジャーバージョンアップなので色々と改良点や新規機能がある訳ですが、やっぱり気になるのはRubyMotionへの対応具合です。

  • メソッドのキーワード引数を認識
  • コード補完対応
  • RubyMotionコンソール(REPL)に対応
  • デバッガ対応

これまでも、Sublime Text 2でコード補完を実現するパッケージが作成されたりしていましたが、RubyMineを使い慣れているプログラマにとって、RubyMotion開発でもRubyMineが使えるのはとても喜ばしいですね。

また、EAPのEnokiは最近アップデートされ、build 124.67になりました。
このアップデートにより、RubyMine上のコンソールでのエラー出力の表示やRubyMineのデバッガを使ったデバッグができるようになっています。
エラー出力がされないのは超ツラかったので、非常に嬉しいですね。

という感じで、結構なキャッチアップ具合と言えるでしょう。JetBrains、相変わらず良い仕事です。

プロジェクトを作成してみる

まず、RubyMotionプロジェクトを作成します。

通常はコンソールで

motion create ExampleText

とかやる訳ですが、RubyMineのメニューから File > New Project … としても作成できます。

プロジェクト新規作成

プロジェクト生成

なお、コンソールで作成したプロジェクトは、File > Open Directory… で開けばRubyMotionプロジェクトとして認識されます。
(古いバージョンのRubyMotionで生成されたプロジェクトだと認識されないことがあります)

サンプルアプリ

今回は、サンプルとして楽天の商品検索APIの結果をテーブルビューで表示するアプリを使います。
ソースはこちらに置いてあります。
サンプルコード

今回のアプリではHTTPでデータを取得する必要があるのですが、HTTPアクセスにはBubbleWrapを使っています。

エディタ機能いろいろ

プロジェクトを作ったら、あとはせっせとコードを書きます。

RubyMotionではCRubyとは少し異なるシンタックスが使われます。
代表的なのはメソッドのキーワード引数ですね。
CRubyも2.0からは導入されることになっていますが、RubyMotionでは(MacRubyでも)既に使われています。

メソッドのキーワード引数

4.5以前のRubyMotionでムリヤリRubyMotionプロジェクトを開くと、キーワード引数の部分が構文エラー扱いになっていましたが、正しいシンタックスとして認識されています。

また、コード入力中の補完も効いています。

コード補完

実際使ってみると、補完候補がまだこなれていない印象が拭えませんが(キーワード引数の補完がうまくいなかいとか)、今後のアップデートに期待したいですね。
ともあれ、多くの場面でかなりコーディングが楽になりました。

ビルド&実行

このソースをビルドするとこんな感じに。

ビルド&実行

ちなみに、シミュレータでの実行結果はこうなります。
楽天の商品検索APIで「Ruby」を検索して、商品名とサムネイル画像をテーブルビューで表示しています。

シミュレータ

我々がこよなく愛するプログラミング言語はどこにも出てきません。
わざわざ「ルビー」で検索してくれる楽天APIには頭が下がります。

RubyMotionの目玉機能の一つである実行中のコンソール(REPL)にも対応ています。 ただ、反応はかなり遅いです。愛なくしては使えない感じです。クールガイにも使えるよう改善して欲しいですね。

実行時コンソール

デバッガ

デバッグモードでの実行では、ブレークポイントを使ったデバッグも可能です。

ブレークポイント

こんな感じで設定して、

デバッガ表示

こんな感じでウォッチできます。

特に設定は必要なく、RubyMotionプロジェクトとして認識されていれば、デバッグ実行するだけでOKです。超お手軽です。

ただ、色々いじってると、シミュレータのプロセスとの接続が切れてしまうことが多いです。
ここでも少なからず愛が必要ですが、現状でもまぁ十分助かります。
RubyMineでWebrick上のRailsアプリをデバッグしていても同様のdisconnectionは発生しますが、RubyMotionデバッグ時の方がキレやすい印象です。最近の中学生のようです。

まとめ

EAPの最初のバージョンはエラー出力がコンソールに出てこないとか、苦行のオモムキもありましたが、先日のバージョンアップでかなり良い感じになってきています。
一通りの開発作業が行えるので、「ごめん、さすがにムリ」な時は普通にiTermとか使うとかして、メイン環境として十分使える状態だと思います。 RubyMineに慣れているプログラマだけでなく、あまり触ったことの無いプログラマも一度試してみてはいかがでしょうか。

Github Pagesにブログを作ってみた

Octopressを使って、Github Pagesにブログを作ってみました。

下記のページが参考になりました。感謝。

Markdown を使って書けることと、
オフラインでの編集・管理がやりやすいのが良いですね。

カスタマイズは広範囲にわたっているので、
あまり時間をかけすぎないように気をつけないと…

テーマは oct2-orange をとりあえず設定。