テーブルビューにセルを追加するためのボタンを配置する
まずは、白紙のテーブルビュー画面に、セルを追加するための「+」ボタンを配置するところから始めましょう。
今回は、セルをタップした際に別のビューに切り替えたり、内容をさらに深く掘り下げるためのテーブルビューを表示するようなナビゲーション機能は使いません。それにもかかわらず、ナビゲーションコントローラーを用意してその中にテーブルビューを配置する形を採用します。それは、ほかでもないナビゲーションバーを使いたいからです。ナビゲーションバーの左右に、「+」ボタンや、後で出てくるような「編集」ボタンを配置するのです。いわばナビゲーションバーをツールバー代わりに使うことになります。iOSアプリとしてはかなり一般的な使い方です。
ナビゲーションバーに特定の機能を持ったボタンを配置するためのコードは、たった1行で済みます。例えばテーブルビューコントローラーのオブジェクト、pgtvcがナビゲーションコントローラーのルートビューコントローラーとして設定されているとすると、以下のようにすればいいのです。
pgtvc.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: pgtvc, action: #selector(pgtvc.addItem(_:)))
ここでは、ナビゲーションバーの左端にボタンを配置するため、navigationItemのleftBarButtonItemプロパティにUIBarButtonItemオブジェクトを設定しています。セルを追加するための「+」ボタンは、UIBarButtonSystemItem.addとして、iOSのシステムにあらかじめ用意されているので、それを指定しています。
また、このボタンの設定の中では、ボタンが押された際のアクションも指定しなければなりません。ここでは、テーブルビューコントローラーpgtvcの中で定義されている(はずの)addItem()ファンクションを呼び出すように設定しています。
このボタンに対応するファンクションが存在していないとプログラムを起動できないので、とりあえず中身は空のまま定義してこれまでの成果を確認しましょう。
この画面には、ここまでに説明したコードが見えていますが、テーブルビューとして基本的な部分は見えません。この例では、初期状態では空のTo Dosという文字列の配列の内容を表示するようになっています。テーブルビューのnumberOfRowsInSectionのファンクションでは、このTo Dos配列の要素の数を返します。また、cellForRowAtファンクションでは、この配列の中身をセルのテキストに設定して返しています。
テーブルビューにセルを追加するファンクションの中身を実装する
ここまでのところでは、空のテーブルビューの上に「ToDoリスト」というタイトルのナビゲーションバーが表示され、その左端に「+」ボタンが配置されました。まだ何も操作することはできません。このボタンを押したときに、実際にセルが追加されるようにするにはaddItem()ファンクションの中身を記述します。
ここでは、以前にWebViewに表示するページのURLの入力に使ったのと同様のアラートを使って、セルに表示するテキストを入力することにしましょう。テーブルビューには、特にセルを追加する機能は用意されていません。それではどうやってセルを追加すればいいのでしょうか?
実はテーブルビューに表示するデータを保持する配列に要素を追加するだけです。テーブルビューは、常にその配列の中身をセルに表示するようにできているので、セルの追加は、いわば自動的に処理されます。ただし配列の中身を変更した際には、テーブルビューのreloadData()というファンクションを呼んで、配列の中身と表示を同期させます。以上のaddItem()のコードを以下に示します。
func addItem(_ sender: Any) {
let pgAlert = UIAlertController(title: "追加", message: "内容を入力してください", preferredStyle: .alert)
pgAlert.addTextField { (textField) in
textField.text = ""
}
pgAlert.addAction(UIAlertAction(title: "Add", style: .default, handler: { [weak pgAlert] (_) in
self.To Dos.append(pgAlert!.textFields![0].text!)
self.tableView.reloadData()
}))
self.present(pgAlert, animated: true, completion: nil)
}
アラートの表示とテキスト入力に関する部分は、以前のWebViewの例とほとんど同じなので説明を省きます。入力されたテキストは、配列オブジェクトのappend()メソッドを使って新たな要素として追加しています。その後でreloadData()メソッドを呼んでテーブルビューの表示を更新するのは、すでに説明したとおりです。
実際に動かしてみましょう。今度は「+」ボタンをタップすると、テキストフィールドを備えたアラートが表示されます。横向き画面では、キーボードを表示するとアラートが画面からはみ出してしまうので、縦向きにして入力するといいでしょう。
ここに適当な文字列を入力して「Add」をタップします。
すると、その文字列がテーブルビューのいちばん上のセルに表示されるはずです。
さらに「+」ボタンをタップして、項目を追加することができます。新たに追加した項目は、もとにあった項目の下に表示されることになります。
テーブルビューを編集するためのボタンを配置する
続いて、テーブルビューの構成を編集するためのボタンを追加してみましょう。「+」ボタンは左端だったので、今度は右端に配置しましょう。「+」ボタンにならってこれも1行で追加できます。ここではnaviationItemのrighBarButtonItemとして、やはりシステムに定義されたeditButtonItemを設定しています。
pgtvc.navigationItem.rightBarButtonItem = pgtvc.editButtonItem
「+」ボタンの場合には、押された際に呼び出すファンクションを指定していましたが、編集ボタンでは不要です。なぜなら、テーブビューの編集機能はシステムが提供してくれるので、自分でファンクションを用意する必要がないのです。
とりあえずボタンだけ追加した状態で動かして、ボタンの配置を確認しておきましょう。
テーブルビューを編集するためのメソッドの中身を実装する
これでナビゲーションバーの右端に「編集」ボタンが表示されるようになりましたが、それだけではまだ機能しません。これを機能させるには、システムが規定している2つのメソッドの中身を記述する必要があります。
1つは、テーブビューの中の個々のセルについて、それが編集可能かどうかの問い合わせに答えるcanEditRowAtというものです。とりあえず、これには常にtrue、つまり編集可能であると返すことにします。
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
もう1つは、実際に編集操作を実行するcommitというもので、その中では指定された位置のセルに対応する配列の中身を削除し、そのセル自体も削除します。テーブルビューの編集とはとりあえず行を削除をするという意味だと考えていいでしょう。配列の要素はremove()メソッドで、セルはdeleteRows()メソッドを使って削除できます。
override func tableView(_ tableView: UITableView, commit editingStyle:
UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
To Dos.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
以上のメソッドをテーブルビューコントローラーの中に記述してから動かしてみましょう。「編集」ボタンをタップすると、編集可能なセル(ここでは全部)が右側に少しスライドして、左端には赤丸の中に「−」の入ったボタンが表示されます。
この赤丸ボタンをタップすると、そのセルの右端には「削除」ボタンが表示されます。
右端の「削除」ボタンをタップすると、そのセルは削除されます。連続して複数のセルを削除することも可能です。
編集中は「編集」ボタンは「完了」というタイトルに変化しています。編集が終わったら、この「完了」をタップすると元の表示に戻ります。当然ながら編集結果が反映され、削除された行はもう表示されません。
セルの順番を入れ替えるためのメソッドを追加する
ここまでは編集=削除でしたが、実はテーブビュービューの編集には、もう1つの機能があります。それはセルの移動です。これも2つのメソッドを追加するだけで実現可能です。
1つは、やはり特定のセルについて、それが移動可能かどうかの問い合わせに答えるcanMoveRowAtというものです。ここでも常にtrueを返すことにします。
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
もう1つは、実際に移動を実行するmoveRowAt toというもので、sourceIndexPathで指定された位置のセルの中身を、destinationIndexPathで指定された位置に移動します。ここでは、swap()ファンクションを使って、2つの位置の配列の中身を入れ替えています。配列名の前に&記号を付けているのは、配列の要素の値ではなく要素そのものを取り出して入れ替えるためです。配列を操作したあとは、reloadData()を呼んで表示を更新しています。
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
swap(&To Dos[sourceIndexPath.row], &To Dos[destinationIndexPath.row])
tableView.reloadData()
}
以上2つのメソッドを記述してからプログラムを動かしてみましょう。「編集」をタップすると、こんどはセルの右端に「≡」のようなボタンが表示されます。これはセルをドラッグして移動するためのグリップのようなものです。
どれかのセルをドラッグして移動してみましょう。
ドラッグによって位置を自由に移動できます。ここで「完了」をタップすれば、移動が確定します。
実は上に示したプログラムには1つ重大なバグがあります。それはセルを移動しようとしてドラッグを始めてから元の位置に戻すと、エラーが発生してプログラムが落ちてしまうことです。
これは、配列の要素の入れ替えの移動元と移動先が同じだとswap()ファンクションが機能しないからです。これを防ぐには、if文を追加して移動元と移動先が同じでない場合だけswap()を呼ぶようにすればいいだけです。
if sourceIndexPath != destinationIndexPath {
swap(&To Dos[sourceIndexPath.row], &To Dos[destinationIndexPath.row])
tableView.reloadData()
}
今回作成したプログラムのソースソースコードはこちらからダウンロードできます。
次回の予定
今回は、テーブルビューのとりあえずの締めくくりとして、セル構成の編集機能(内容の追加、削除、順番の入れ替え)を実現してみました。To Doリストに限らず、さまざまな用途に応用できるでしょう。このところテーブルビューの話がずっと続き、込み入った内容も多かったので、少し飽きてきたころかもしれません。次回は少しすっきりした題材を取り上げたいと思います。Swift Playgrounds 1.2から利用可能になったMapKitを使って地図を表示するというわかりやすいプログラムを作成する予定です。