Fork me on GitHub

Tables

私たちはビュー、コントローラ、コンテナコントローラと順にたどってきましたが、ここでは道を逸れもう少し独特なものを得たいと思います。UITableView クラスはほとんどの iOS アプリにとって非常に重要なものです。 1 つの章 (たぶんもっと多くの章が必要でしょう!) として十分値するものです。

みなさんの iPhone や iPad に搭載されているデフォルトのアプリに目を通してみましょう。青や黒のバーよりもさらに共通の傾向があることがわかるでしょう。それはスクロールするリストです。Phone.app は巨大なコンタクトリストをなめらかで使いやすくスクロールできます。SMS.app はほとんど青と緑のメッセージをスクロールできます。見た目はまったくことなりますが、これらは同じツールで構築されています。それは UITableView です。

UITableView

UITableView はプレゼンテーションと似ていて、スクロールするコンテンツを構築します。UITableView は非常にたくさんのアイテムを持つことができます。ごく少数から非常に大量のアイテムまでスケールする秘訣は、常に決められた一定数のサブビューのみをメモリで使用するからです。複雑そうなので、細かく見ていきましょう。

Mail.app で、みなさんは受信トレイに何百通もメールがある可能性がありますが、画面上にはその一部 (6 通くらい) しか表示されません。スクロールすると、一番上のメールはゆっくりと画面外へ移動します。テーブルビューは隠れてしまったこれらのビューを持ち、そして画面の 反対側 の端へそれらを移動します。画面の上部を移動したこれらのメッセージビューは、画面の下部に再配置されます。そして配置された新しい位置に適したデータで内容が置き換えられます。テーブルビューは信じられないほど高速にそれらの行を移動します。そのため、テーブルに数百ものアイテムがあったとしても、ひとにぎりの行のビューがメモリに保持されているだけとなります。

わかっていただけたと願います。

それでは、このすばらしいクラスをどのように利用するのでしょう?UITableView はほかの UIView のようにビューなので、適切な frame を持ったサブビューとして追加する必要があります。また、delegatedataSource オブジェクトを割り当てる必要があります。これらのオブジェクトはいくつかのメソッド (必要に応じてもっと多く) の実装を必要とします。テーブルビューのすべての行を描画するために必要な情報を取得するため、テーブルビューはオブジェクトのそれらメソッドを呼び出します。どうだろう、わかった?コードを書いてみましょう。

Containers の章で作成したほかの UIViewController を覚えていますか?データを渡すために UITableView を使用するコントローラで置き換えていきます。そのコントローラクラスを作っていきましょう。

./app/controllers/AlphabetController.rbAlphabetController を作成し、次の初期段階の実装を行います。

class AlphabetController < UIViewController
  def viewDidLoad
    super

    self.title = "Alphabet"

    @table = UITableView.alloc.initWithFrame(self.view.bounds)
    self.view.addSubview @table
  end
end

今ではおなじみのものですね。view.bounds が少し新しいでしょうか。view.frame のように、ビューの bounds が CGRect オブジェクトで返されます。しかし、それは空の座標原点となっています。基本的に、それは位置情報を持たないビューのサイズを示します。それで newView.initWithFrame(view.bounds) を実行するときに、newView に自身のサイズで view を覆い尽くすようになります。

AppDelegate へ戻り、タブコントローラをこの新しいコントローラを使用するように変更しましょう。

  ...
  controller = TapController.alloc.initWithNibName(nil, bundle: nil)
  nav_controller = UINavigationController.alloc.initWithRootViewController(controller)
  alphabet_controller = AlphabetController.alloc.initWithNibName(nil, bundle: nil)

  tab_controller = UITabBarController.alloc.initWithNibName(nil, bundle: nil)
  tab_controller.viewControllers = [alphabet_controller, nav_controller]
  @window.rootViewController = tab_controller
  ...

必要なことは、新しく AlphabetController を作成し、tab_controllerviewControllers でそれを先頭に配置することです。rake を実行すると、愛しの (空っぽだけど) テーブルビューを見ることでしょう。

First table view

AlphabetController を記述していく時間です。まずはじめに、テーブルビューの dataSource としてコントローラを viewDidLoad の最後で設定する必要があります。

...
  def viewDidLoad
    ...
    @table.dataSource = self
  end
...

data source は以下のメソッドを実装しなければなりません。

  • tableView:cellForRowAtIndexPath:
  • tableView:numberOfRowsInSection:

使用できるメソッドはもっとありますが(Apple's docs でチェックしてください)、最も基本的なテーブルビューの dataSource は上記のメソッドだけが必要です。それでは、早速それらを用意しましょう。

  def tableView(tableView, cellForRowAtIndexPath: indexPath)
    # return the UITableViewCell for the row
  end

  def tableView(tableView, numberOfRowsInSection: section)
    # return the number of rows
  end

cellForRowAtIndexPath: は馴染みの無い 2 つのオブジェクト使います。一つは NSIndexPath クラスの indexPath、もう一つはメソッドが返す UITableViewCell クラスのオブジェクトです。NSIndexPathsectionrow というプロパティを持っていて、何行目のデータを私たちが与えるのかを示します。

UITableViewCell は実際に私たちのテーブルに表示される UIView のサブクラスです。デフォルトの実装では、textLabelimageView といったいくつかの便利なフィールドが付属します。独自の見た目や振る舞いを追加するために UITableViewCell をサブクラス化できますが、通常のクラスを使用して理解を得ることができるでしょう。

cellForRowAtIndexPath は、新しいセルを作成するか画面外に移動したセルを再利用し indexPath で示されるデータを設定したのち、セルのオブジェクトを返す必要があります。実装したものは次のようになります。

  def tableView(tableView, cellForRowAtIndexPath: indexPath)
    @reuseIdentifier ||= "CELL_IDENTIFIER"

    cell = tableView.dequeueReusableCellWithIdentifier(@reuseIdentifier) || begin
      UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier)
    end

    # put your data in the cell

    cell
  end

reuseIdentifier とは何でしょう?ええと、UITableView はテーブルで使用される各セルの種類ごとに識別子を与えることで「再利用」する細工がなされています。再利用するセルを取得するとき、実際には同じ識別子を持つセルをプールしているところからそれらを取得します。そのため、もし何らかの理由によりすべての行に対してユニークな識別子を与える場合、識別子ごとのプールにはセルが 1 つずつしかないため再利用できるセルは取得できません。もし視覚的にそして種類も全く異なる 2 つの行を扱う場合には、おそらく 2 つの識別子を持つことになるでしょう。わかりました?

みなさんは ||= 演算子を知らないかもしれませんね。Ruby では、a ||= b は 「もし a が nil か false なら、b の値を代入する」という意味になります。上の例のように、オブジェクトにデフォルト値を与える、あるいは確実に 1 度だけ代入する良い方法です。

それで、識別子を指定して UITableViewCell を取得するために dequeueReusableCellWithIdentifier: を使用します。もしセルが 1 つも無ければ(dequeueReusableCellWithIdentifier:nil を返します)、begin/end のブロックが実行されます。このブロックで、セルのスタイル (詳しくはこちらを参照) と reuseIdentifier を指定し新しく UITableViewCell を作成します。cellForRowAtIndexPath メソッドの終わりでセルを返します。

次に何をするか予測がつかないのでしたら、私たちはテーブルにアルファベットの文字を埋め込んでいくつもりです。たいして役に立たないし難しくないけど、セルが再利用されるのを確認できるようにセットアップが簡単にできます。よりクリエイティブなデータを自由に使ってみてください(^_^)

viewDidLoad で、行のタイトルとして使用する配列を初期化しましょう。Ruby は大変すばらしいので、1 行で素早くそれができます。

def viewDidLoad
  ...
  @data = ("A".."Z").to_a
end

すべての大文字アルファベットが含まれた配列が作成されます。すばらしい、そうでしょ?

私たちは 2 つの data source メソッドを実装する必要があります。numberOfRowsInSection: は、@data を使うことしかしていないのでかなり単純です。

def tableView(tableView, numberOfRowsInSection: section)
  @data.count
end

cellForRowAtIndexPath: で、セルの textLabel に対応する @data の文字列をシンプルに設定します。

def tableView(tableView, cellForRowAtIndexPath: indexPath)
  ...
  cell.textLabel.text = @data[indexPath.row]

  cell
end

rake を実行し、チェックしてみましょう。

The alphabet in a table-view

悪くないよね?もう 1 つ作業し、ユーザの操作を受け付けるようにしてみましょう。

UITableViewdataSourcedelegate を持っていると言ったのを覚えているかな?AlphabetControllerdelegate も作ってみましょう。行をタップすると何かアクションすることができます。

UITableViewdataSource メソッドは一般的にテーブルへ情報やデータを提供します。delegate メソッドはテーブルの見た目や読み込まれたデータに対するユーザの操作に関係します。ユーザによって行がタップされたことを検出するためにこれら delegate メソッドの 1 つを使い、そしてアラートを表示します。

viewDidLoad でコントローラの delegate を実際に用意してみましょう。

def viewDidLoad
  ...
  @table.dataSource = self
  @table.delegate = self
  ...
end

テーブルの delegate では必須なメソッドはありません。必要に応じて自由にいくつか 実装しましょう(必要がなければ実装しなくても良いです)。ユーザによってタップされた行を見つけ出すために、delegate メソッドの tableView:didSelectRowAtIndexPath: を使います。

def tableView(tableView, didSelectRowAtIndexPath:indexPath)
  tableView.deselectRowAtIndexPath(indexPath, animated: true)

  alert = UIAlertView.alloc.init
  alert.message = "#{@data[indexPath.row]} tapped!"
  alert.addButtonWithTitle "OK"
  alert.show
end

デフォルトでは、UITableView はタップされた行を青色でハイライトし続けるので、それを解除するために deselectRowAtIndexPath:animated が必要になります。それ以外の部分は、これまでに扱ってきたものばかりですね。

さぁ、いっきに rake を実行してみましょう。

Opening a popup when a row is tapped

W-w-w-wrap up

UITableView の基本についてカバーしました。より良いものにするためにカスタマイズや振る舞いの変更を行うことができます。より詳細な記事についてはいつの日にか。

今日学んだことは何だったでしょう?

  • UITableView は効率的な方法で似たようなデータを行に表示します。
  • UITableView には情報を提供するために dataSource が、そしてユーザの操作と look-and-feel を扱うために delegate があります。
  • dataSource には 2 つの実装が必須なメソッド numberOfRowsInSectioncellForRowAtIndexPath があります。cellForRowAtIndexPathUITableViewCell のインスタンスを返す必要があります。
  • ユーザが行をタップしたことを検出するために delegate メソッドの didSelectRowAtIndexPath を使うことができます。

白鳥は次の行へ飛び込み、アニメーションについて学びます!


Like it? Spread the word