Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 332 - Table View チュートリアル

2017年3月24日:Xcode 8.2で動作するように変更*1

f:id:yataiblue:20151231144735j:plain BASIC テーブルビュー実装の基本

Table Viewの実装法(まとめ)

UITableViewを扱う場合、UIViewControllerに直接コードで実装する方法もあれば、Xcodeを使ってMain.storyboardに実装する方法もあります。要するに色々あるわけです。ストーリーボードでUITableViewCellのBasicを使用する実装法が簡単なので最初に説明します。それからコードで実装する方法も解説します。

新しいXcodeプロジェクトを作ります(BASIC 新規プロジェクト作成手順)。「Single View Application」を選択して、「SimpleTableView」という名前を付け、言語やターゲットはSwiftとUniversalにします。

シンプルな実装にするので、ViewController名は変えずそのまま使用します。

TableViewもPickerと同様で、デリゲーションを必ずコードしなければならないので、その準備をしていきます。

まず、Main.storyboardを選択して、オブジェクト・ライブラリからTable ViewをドラッグししてViewControllerに設置します。Table ViewのサイスをViewControllerのサイズに広げ、HIG*2に準拠した青い点線が出現するまで変更します。

Pin ボタンコンストレイントを設定して、サイズ・インスペクタで確認します。マジックナンバー(Magic Number)があれば修正をします。

トーリーボードでCellを生成する方法

オブジェクト・ライブラリからドラッグして設置したTable ViewにはCellが表示されていません。Tabel Viewが選択された状態でアトリビュート・インスペクタの「Prototype Cells」を「1」に変更します。

f:id:yataiblue:20170324161436j:plain

トーリーボードに出現したCellを選択して、アトリビュート・インスペクタの「Style」から「Basic」を選択して「Identifier」に「SimpleTableViewCell」を入力します。

f:id:yataiblue:20170324180044j:plain

これでCellの準備が整いました。

プロトコールをプロパティとして持つ

設置したTable Viewを「Ctrl+ドラッグ」して、Main.stroyboard上部の1番左に位置するViewController:f:id:yataiblue:20160205105850j:plain で「リリース」します。

そしてDelegateDataSourceのプロパティを持たせます*3

プロトコールに準拠

ViewControlleにプロトコールに準拠させるステップです*4

class ViewController: UIViewController,
                          UITableViewDataSource,
                              UITableViewDelegate {

必須メソッドの実装

最後に必須メソッドの具体的な実装です*5

TableViewの場合、実装すべきメソッドは2つだけです。

まずDataSourceはTableに表示する項目数を指定します。

その前に、Table Viewに表示するデータをデモらしくViewControllerに保持させます。普通のアプリならプロパティリスト(plist)を使うのが一般的でしょう*6

private let itemes = ["Sleepy", "Sneezy", "Bashful", "Happy",
  "Doc", "Grumpy", "Dopey", "Thorin", "Dorin", "Nori", "Ori",
  "Balin", "Dwalin", "Fili", "Kili",
  "Oin", "Gloin", "Bifur", "Bofur", "Bombur"]

itemesというアレーを用意します。

するとメソッドは次のようになります。

// 必須メソッド1
func tableView(_ tableView: UITableView, 
         numberOfRowsInSection section: Int) -> Int {
    return itemes.count
}

次のDelegateメソッドの実装には少し説明が必要です。TableViewが表示する1行はUITableViewCellです。これはViewクラスのサブクラスなので、TableViewはViewクラスの集まりというイメージです。項目が多いとかなりメモリーを消費してしまうため、画面に表示されているTableViewCellだけメモリ管理するようにします。画面から消えたTableViewCellはテンポラリーなdequeueCellに入れて管理から外し、メモリーに余裕がある間だけデータを保持して再表示のためにアクセスします。

// 必須メソッド2
func tableView(_ tableView: UITableView, 
         cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(
                          withIdentifier: "SimpleTableViewCell", 
                                     for: indexPath)
    cell.textLabel?.text = itemes[indexPath.row]
    return cell
}

これでランすればテーブルができています。

f:id:yataiblue:20170324180259j:plain

コードでCellを生成する方法

Main.storyboardのTableViewにPrototype Cellが表示されていれば消す必要があります。TableViewが選択された状態でアトリビュート・インスペクタを開いて「Prototype Cells」の項目を「0」にします。

TableViewCellをコードで生成するためにもIdentifierは必要になります。Identifierを使ってTableViewCellのインスタンスを作るからです。

let simpleTableIdentifier = "SimpleTableIdentifier"

Identifierを用意するだけでは、TableViewCellが作れません。TableViewクラスが持っているregisterメソッドを使ってTableViewCellを生成する必要があり、Main.storyboardにあるTableViewを「Ctrl + ドラッグ」して、@IBOutletプロパティとしてコード化します(BASIC UI部品のコード化(@IB))。

@IBOutlet weak var simpleTableView: UITableView!

次にTableViewCellのインスタンス作成のステップですが、iOS開発のバリエーションを説明します。プログラミング初心者が混乱しやすいポイントだと思います。最も原始的なインスタンス化は「コーディング」です。コードを書いてインスタンス化させる方法なんですが、コーディングはタイプ量も多くなり時間もかかります。それを自動化したのがXcodeの機能を使う前述したやり方です。

コーディングで実装する方法の場合、TableViewクラスの「resister」メソッドを使って、simpleTableIdentifierを指定してTableViewCellインスタンスを作ります。初期化メソッドのviewDidLoad()内にコードすることでアプリが立ち上がった時に生成されるんです。

override func viewDidLoad() {
    super.viewDidLoad()

    simpleTabelView.register(UITableViewCell.self, 
        forCellReuseIdentifier: simpleTableIdentifier)
}

そしてデリゲーションメソッド、必須メソッド2の「withIdentifier」の引数を「simpleTableIdentifier」に変更します。

これでCellのインスタンス化ができ、ランすれば表示されます。

どちらがあなたのプログラミングスタイルに合っていますか? 私はXcodeを使って実装する方法ですね。

iOS開発では、1つの機能を実現させるために幾通りも方法があることを学びました。

次はTableViewCellに画像を設置するのですが、そのまま本についているイメージを使います*7

Images.xcasetsに2つのイメージをドラッグ&ドロップします。

f:id:yataiblue:20170324195911j:plain

TableViewCellはデフォルトな状態でイメージを扱えます。

デリゲートメソッドを変更します

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = 
        tableView.dequeueReusableCell(
            withIdentifier: simpleTableIdentifier, 
                       for: indexPath)
    cell.textLabel?.text = itemes[indexPath.row]
    let image = UIImage(named: "star")
    let higlightedImage = UIImage(named: "star2")
    cell.imageView?.image = image
    cell.imageView?.highlightedImage = higlightedImage
        
    return cell
}

これだけで次のようになります。

f:id:yataiblue:20170324200136j:plain

次はCellのスタイルを変更させるのですが、ここでもiOS開発の多様性を垣間見ることができます。

Xcodeを使ったスタイル変更は結構簡単にできます。まずスタイル変更に併せて詳細情報を表示させる必要があるので詳細情報をCellに加えます。

テーブルの最初から7つの項目に「Mr Disney」その後は「Mr Tolkien」という情報をCellの持っているdetailTextLabelプロパティに加えるコードをメソッド内に加えます。

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = 
        tableView.dequeueReusableCell(
            withIdentifier: simpleTableIdentifier, 
                       for: indexPath)
    cell.textLabel?.text = itemes[indexPath.row]
    let image = UIImage(named: "star")
    let higlightedImage = UIImage(named: "star2")
    cell.imageView?.image = image
    cell.imageView?.highlightedImage = higlightedImage

    // ここにコードを加えます。
    if indexPath.row < 7 {
        cell.detailTextLabel?.text = "Mr Disney"
    } else {
        cell.detailTextLabel?.text = "Mr Tolkien"
    }
        
    return cell
}

このままランしても詳細情報は表示されません。Main.storyboadでCellを選択するのですが、storyboardでクリックして選択するのはコツがいるので、ドキュメント・アウトラインから「SimpleTableIdentifier」を選択するといいでしょう。それが選択されている状態でアトリビュート・インスペクタの「Style」から「Subtitle」を選びます。

そしてランすると最初の7つに「Mr Disney」、その後は「Mr Tolkien」が表示されています。

f:id:yataiblue:20170324201156j:plain

「Style」から「Right Detail」を選ぶと

f:id:yataiblue:20170324201444j:plain

「Style」から「Left Detail」を選ぶとなぜかイメージは消えちゃいます。

f:id:yataiblue:20170324203421j:plain

非常に簡単に変更できるんですが、これは開発の多様性の説明になっていません。コーディングで実装する方法を説明します。まず、Main.storyboardからTableView(TableViewCellではありません)を選んでアトリビュート・インスペクタの「Prototype Cells」を「0」にします。

そして、デリゲートメソッドの中で使っている次のコード

let cell = 
    tableView.dequeueReusableCell(
        withIdentifier: "SimpleTableViewCell", 
                   for: indexPath)

これを次のように変えて、Cellインスタンスを生成するイニシャライザに切り替えます。当然Identifier「let simpleTableIdentifier = "SimpleTableIdentifier"」を用意しなければなりません。

let cell = 
    UITableViewCell(style: UITableViewCellStyle.default, 
          reuseIdentifier: simpleTableIdentifier)

しかし、このままではスタイルがデフォルトなので表示が変わりません。引数として与える「style」を変更する必要があります。

Xcodeの「Style」の記述を異なるので対比させると

Xcodeの「Default」は、コードで「UITableViewCellStyle.default」
Xcodeの「Subtitle」は、コードで「UITableViewCellStyle.subtitle」
Xcodeの「Right Detail」は、コードで「UITableViewCellStyle.value1」
Xcodeの「Left Detail」は、コードで「UITableViewCellStyle.value2」

TableViewDelegateのオプションメソッドを使う

TableViewの「見た目」をコントロールするのがUITableViewDelegateのオプショナルなメソッドです。

色々なメソッドが用意されているのですが、インデントを調整するメソッドは応用されることが多いようです。しかし、このメソッドでのスタイル変更をするのであれば、Cellインスタンス作成のためにUITableViewCell()イニシャライザを使う必要があります。Prototype Cellsを使って作成した場合は、設定変更がアトリビュート・インスペクタで制限されてしまうようです。

インデントをコントロールするオプショナル・メソッドです。

func tableView(_ tableView: UITableView, 
    indentationLevelForRowAt indexPath: IndexPath) -> Int {
    return indexPath.row % 4
}

indexPathに数を与えることで右へインデントをずらしていきます。最初の行(0)は「0」を返し、次の行(1)は「1」を返します。そして「2」「3」と続き、5行目に再び「0」を返します。すると次のようになります。

f:id:yataiblue:20170324205838j:plain

TableViewCellを選択したときに反応を返すためのデリゲーションメソッドも当然用意されています。

選択される前に制御するメソッドと選択されてから制御するメソッドがあります*8

func tableView(_ tableView: UITableView, 
            willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    if indexPath.row == 0 {
        return nil
    } else {
        return indexPath
    }
}

これは選択前の制御です。indexPath.rowが「0」というのは、1番上の行にあるcellで、これが選ばれれば「nil」を返して無反応にさせています。部分的に反応を制御させるのに利用します。

func tableView(_ tableView: UITableView, 
             didSelectRowAt indexPath: IndexPath) {
    let rowValue = itemes[indexPath.row]
    let message = "You Selected \(rowValue)"
        
    let controller = 
        UIAlertController(title: "Row selected", 
                        message: message, 
                 preferredStyle: .alert)
    let action = UIAlertAction(title: "Yes I did", 
                               style: .default, 
                             handler: nil)
    controller.addAction(action)
    self.present(controller, animated: true, 
                           completion: nil)
}

これはcellが選択された後に実行されるコードです。もう何度も繰り返し練習したアラートメッセージです。選択された行を呈示します。

f:id:yataiblue:20170324213451j:plain

TableViewCellの選択前制御のところで、解説の追記があります。

選択しようとする行を変更するよな場合、(_, willSelectRowAtIndexPath:)を使うのが一般的で、indexPathの値を変更させないでIndexPath(forRow:, inSection:)を使って新しいindexPathオブジェクトを生成する方がいいようです。

偶数行を選択すると、1つ下の行が選択されるコードのデモです。

func tableView(_ tableView: UITableView, 
    willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    if indexPath.row == 0 {
        return nil
    } else if indexPath.row % 2 == 0 {
        return IndexPath(row: indexPath.row + 1, 
                     section: indexPath.section)
    } else {
        return indexPath
    }
}

TableViewCellのフォントサイズを変えたり行の幅を変えることもできます。フォントのサイズは、お馴染みの必須メソッドの中でプロパティを変更します。

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: UITableViewCellStyle.default, 
                     reuseIdentifier: simpleTableIdentifier)
    ....

この中に次の行を入れるだけで、メインのフォントサイズが大きくなります。

cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 50)

スタイルが「default」だと

f:id:yataiblue:20170324214424j:plain

スタイルを「subtitle」に変更すると

f:id:yataiblue:20170324214811j:plain

iOS8以降で、フォントサイズに合わせて行の幅は自動的に広がるという話ですが、上手くいってないので行幅も調整してみます。次のコードも加えます。

func tableView(_ tableView: UITableView, 
    heightForRowAt indexPath: IndexPath) -> CGFloat {
    return indexPath.row == 0 ? 100 : 70
}

f:id:yataiblue:20170324215740j:plain

これでTable Viewの取り扱いの基本事項を終わります。

どうでしたか、皆さん?

*1:2016年4月17日改訂:Xcode7.3で動作確認済み - シュミレータをiPhone6sに変更

*2:iOS Human Interface Guidelines(HIG)」というiOSのユーザーインターフェイスのデザインを規定するガイドラインです。オブジェクトのデザイン、サイズ、位置をアップルが規定することで、様々なアプリに統一感を持たせてユーザーに使用にあたり混乱を生じさせないようにしています。このガイドラインに準じていないとApp Storeにも登録されないようです。→iOS Human Interface Guidelines

*3:さらっと書いていますが、非常に重要なポイントです。このステップの意味が分かっていればiOSプログラミングの中級レベルです。もし理由が分からなければ、Swiftで遊ぼう! - 260 - プロトコールとデリゲーション ProtocolsとDelegation - Swiftで遊ぼう! on Hatenaを読んで6ステップ実装法を理解する必要があります。コードで書いてあるステップ5をXcodeを使って視覚的に設定するやり方をここでさらっと書いている訳です。分かりました?

*4:これは6ステップ実装法のステップ4ですね。

*5:これは6ステップ実装法のステップ6です。

*6:2016年4月22日に説明しました->Swiftで遊ぼう! - 648 - プロパティ・リスト(plist)を作る - Swiftで遊ぼう! on Hatena

*7:販売元のWebページに行けば、サンプルコードがダウンロードできます。その中にある「08 - Star Image」を使います。

*8:この使い分けができれば初心者脱却です。処理に時間がかかり選択後に処理が終了している方が望ましい場合はwillSelectRowAtIndexPathを使うといいようです