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