Teacup の AutoLayout サポートを使ってカスタム UITableViewCell をレイアウトする

RubyMotion 用 gem の Teacup は、CSS っぽくビューのデザインを記述できる、 厳密にはビューのプロパティを設定するための内部 DSL。 なんと AutoLayout もサポートしていたりする。

Teacup の AutoLayout を使って、カスタム UITableViewCell をレイアウトしようとして、 README に書かれていないことで結構苦労したのでメモしておく。

まずスタイルを記述する。

Teacup::Stylesheet.new :item_cell do
  style :title_label,
    constraints: [
      constrain(:left).equals(:superview, :left),
      constrain(:top).equals(:superview, :top),
      constrain(:bottom).equals(:superview, :bottom),
    ]

  style :price_label,
    constraints: [
      constrain(:right).equals(:superview, :right),
      constrain(:top).equals(:superview, :top),
      constrain(:bottom).equals(:superview, :bottom),
    ]

  style :comment_label,
    constraints: [
      constrain(:left).equals(:title_label, :right),
      constrain(:right).equals(:price_label, :left),
      constrain(:top).equals(:superview, :top),
      constrain(:bottom).equals(:superview, :bottom),
    ]
end

constraints の部分が AutoLayout。ここ記述ミスしやすい上に、ミスっていてもなかなか気づかない。今回は1時間ほどハマった。要注意。

次にスタイルを適用するセルを作る。 UIViewController は Teacup が拡張していて stylesheet メソッドが使えるけど、 UIView は拡張されていない。 Teacup::Layout を incude する必要がある。

class ItemCell < UITableViewCell
  CELL_ID = "item-cell"

  include Teacup::Layout
  stylesheet :item_cell

  def self.cellForItem(item, inTableView:tableView)
    cell = tableView.dequeueReusableCellWithIdentifier(CELL_ID)
    unless cell
      cell = self.alloc.initWithStyle(
        UITableViewCellStyleDefault,
        reuseIdentifier:CELL_ID
      )
    end
    cell.setupItem(item)
    cell
  end

  def initWithStyle(style, reuseIdentifier:reuseIdentifier)
    super
    layout self.contentView do
      @titleLabel = subview(UILabel, :title_label)
      @commentLabel = subview(UILabel, :comment_label)
      @priceLabel = subview(UILabel, :price_label)
    end
    self
  end

  def setupItem(item)
    @titleLabel.text = item[:title]
    @commentLabel.text = item[:comment]
    @priceLabel.text = "#{item[:price]}"
  end
end

セルを使うコントローラー。

class MainViewController < UITableViewController
  def self.controllerWithNavigation
    UINavigationController.alloc.initWithRootViewController(self.alloc.init)
  end

  def init
    initWithStyle(UITableViewStylePlain)
    @items = [
      { title: "foo", comment: "aaaaa", price: 100 },
      { title: "bar", comment: "bbbbb", price: 200 },
      { title: "hoge", comment: "ccccc", price: 300 },
      { title: "fuga", comment: "eeeee", price: 400 },
    ]
    self
  end

  def viewDidLoad
    super
    self.title = "Sample"
  end

  def tableView(tableView, numberOfRowsInSection:section)
    @items.size
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    item = @items[indexPath.row]
    cell = ItemCell.cellForItem(item, inTableView:tableView)

    # AutoLayout 適用
    cell.restyle!
    cell.apply_constraints

    cell
  end
end

cell の restyle! と apply_constrains を呼び出さないと、スタイルに記述した制約が適用されない。 README に書いてなくて、これまたハマった。最終的にはソースコードまで読むことに。

最後に、今回作成したサンプルを載せておく。

# coding: utf-8

Teacup::Stylesheet.new :item_cell do
  style :title_label,
    constraints: [
      constrain(:left).equals(:superview, :left),
      constrain(:top).equals(:superview, :top),
      constrain(:bottom).equals(:superview, :bottom),
    ]

  style :price_label,
    constraints: [
      constrain(:right).equals(:superview, :right),
      constrain(:top).equals(:superview, :top),
      constrain(:bottom).equals(:superview, :bottom),
    ]

  style :comment_label,
    constraints: [
      constrain(:left).equals(:title_label, :right),
      constrain(:right).equals(:price_label, :left),
      constrain(:top).equals(:superview, :top),
      constrain(:bottom).equals(:superview, :bottom),
    ]
end

class MainViewController < UITableViewController
  def self.controllerWithNavigation
    UINavigationController.alloc.initWithRootViewController(self.alloc.init)
  end

  def init
    initWithStyle(UITableViewStylePlain)
    @items = [
      { title: "foo", comment: "aaaaa", price: 100 },
      { title: "bar", comment: "bbbbb", price: 200 },
      { title: "hoge", comment: "ccccc", price: 300 },
      { title: "fuga", comment: "eeeee", price: 400 },
    ]
    self
  end

  def viewDidLoad
    super
    self.title = "Sample"
  end

  def tableView(tableView, numberOfRowsInSection:section)
    @items.size
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    item = @items[indexPath.row]
    cell = ItemCell.cellForItem(item, inTableView:tableView)

    # AutoLayout 適用
    cell.restyle!
    cell.apply_constraints

    cell
  end
end

class ItemCell < UITableViewCell
  CELL_ID = "item-cell"

  include Teacup::Layout
  stylesheet :item_cell

  def self.cellForItem(item, inTableView:tableView)
    cell = tableView.dequeueReusableCellWithIdentifier(CELL_ID)
    unless cell
      cell = self.alloc.initWithStyle(
        UITableViewCellStyleDefault,
        reuseIdentifier:CELL_ID
      )
    end
    cell.setupItem(item)
    cell
  end

  def initWithStyle(style, reuseIdentifier:reuseIdentifier)
    super
    layout self.contentView do
      @titleLabel = subview(UILabel, :title_label)
      @commentLabel = subview(UILabel, :comment_label)
      @priceLabel = subview(UILabel, :price_label)
    end
    self
  end

  def setupItem(item)
    @titleLabel.text = item[:title]
    @commentLabel.text = item[:comment]
    @priceLabel.text = "#{item[:price]}"
  end
end

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = MainViewController.controllerWithNavigation
    @window.makeKeyAndVisible
    true
  end
end