MILLEN BOX 2

個人iOSアプリ開発者hollymotoによる勉強の記録。時々雑記。

TabelViewControllerを使ってリスト表示アプリを作る

タスク管理アプリとかメモ帳とか、そうゆうのを作ろうと思うと避けて通れないTabelViewController(またはTableView)。 とは言うものの結構気にしないといけないことが多くて、いざ作成するとなると「何するんだっけ??」とよくなります。 (因みに私の出しているアプリではほとんど使ったことありません…)

今日はそんな苦手意識を取り払うべく、TableViewControllerについて整理していきたいと思います。 長くなりますがご勘弁下さい。

完成イメージ
f:id:anthrgrnwrld:20200212191235g:plain

目次

やりたいことを整理

1枚目画面:
* ボタンを押したらリスト画面(2枚目)に遷移する。

2枚目画面:
* リスト画面 NavigationBarの上部左にEditボタン、右にAddボタンを配置。
* Addボタンを押したらリストにテキスト文字列の項目(便宜上タップした日付)を追加する。
* Editボタンでリストの並び替えが出来たり、リストから項目を消去出来たりする。 * リスト内の項目をタップすると3枚目のViewControllerに遷移する。

3枚目画面:
* ViewControllerの中心に2枚目でタップした項目のテキスト(日付)を表示する。

(余談ですが、これ実はiPad用のSplitViewControllerの部分を除くとXcodeのテンプレートとほぼ同じことしてます。Xcodeとかその他のテンプレートを見てどうやって実装するかと考える作業が結構好きです。勉強なるなーと個人的に思ってるんですがどうでしょうか?)

では作成していきます。

1. 準備 - Stroryboardを作成

1-1. 1枚目のUIViewControllerのStoryboardを作成

UI部品の新規作成メニューからViewControllerを持ってきて、下の画像のような感じでボタンを置けばいい。ボタン位置などは適宜対応して下さい。後々ボタンを押したらリスト画面(2枚目)に遷移するようにします。これはまた後で。
f:id:anthrgrnwrld:20200206215752p:plain:w350

1-2. 2枚目のTableViewControllerのStoryboardを作成

UI部品の新規作成メニューからTableViewControllerを持ってきます。そしてTableViewControllerにNavigationBarを付けましょう。  お手軽なNavigationBarの付け方はこの記事を見てみて下さい。

www.millenbox2.com

1-3. 3枚目のUIViewControllerのStoryboardを作成

UI部品の新規作成メニューからViewControllerを持ってきて、下の画像のような感じでラベルを置いて下さい。 このラベルには2枚目のTableViewControllerで表示した項目のテキストの内容(日付)を表示する予定です。

1-4. 1枚目と2枚目のViewControllerを繋げる

1枚目のUIButtonからControlキーを押しながらビューっと2枚目に繋げる。

そうしたらポップアップでどのように遷移したいか聞かれるので(Action Segueの選択)、好きなのを選んで下さい。 何でも良いかと思いますが、Show Detailあたりがいいんじゃないでしょうか。

そうしたら2枚目のTableViewControllerの上部に全画面のViewControllerが隠れているような表示に変化しました。

これでボタンを押したら2枚目のViewControllerに移動できるようになりました。

1-5. 2枚目と3枚目のViewControllerを繋げる

1-4と同じようにControlを押しながら2枚目のTableViewControllerのCellから3枚目のUIViewControllerにビューっと矢印を繋げて下さい。注意するところはちゃんとTableViewの中のCellを選択してControlを押すところです。よくあるのがCellの中にあるContentsViewを選択していたりして矢印が出ないことです。そうするとビューっと矢印を伸ばすことが出来ませんので…。

そうしたらまた1-4と同じようにSegueの選択画面が現れます。今回はShowを選択しましょう。

Showを選ぶと2枚目で付加したNavigationBarを生きて自動的に戻ると同じ機能を持ったボタンが出来ます。

これでStoryboard作成は完了。

2. ListViewControllerの実装

コードの実装に移ってきます。1枚目のViewControllerについては実装いりません。ボタンを押して遷移するだけなので、Storyboardでボタンから矢印伸ばして繋げた時にそのように動くようになっています。よってコードの実装は2枚目のTableViewControllerから。

2-1. 新規Swiftファイルを作成。

新規Swiftファイルを作成して下さい。これをしなきゃ始まらません。名前は「ListViewController」と名付けました。SourceとしてCocoaTouchクラスを選択した場合、作成時に継承するクラスを選べるかと思いますが、UITableViewControllerを選択するといいかもしれません。コメントアウトされてる部分なんかにdelegateなどの実装しなきゃいけないことが備忘録的に書いていたりするので。そんなもん必要ないぜっという方はUIViewControllerのプレーンなやつを継承してもいいかと思います。

2-2. 「Addボタン」の作成

リストに項目を追加する「Addボタン」を作成します。 こんな感じでいかかでしょうか。

// MARK: - ListViewController
class ListViewController: UITableViewController {
    
    var objects = [Any]()   //Listの元になる配列の方はAny型で定義する

    // (1)
    override func viewDidLoad() {
        super.viewDidLoad()
        setNavigationItem()     //Navigation BarのボタンItemをセットする
    }
    
}

// (2)
// MARK: - extension ListViewController for Private Method
extension ListViewController {
    /**
     Navigation BarのボタンItemをセットする
    */
    private func setNavigationItem() {
        //Navigation Bar 左側
        //Addボタン追加
        let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
        navigationItem.rightBarButtonItem = addButton
    }
    
}

// (3)
// MARK: - extension ListViewController for selector
extension ListViewController {
    
    @objc
    func insertNewObject(_ sender: Any) {
        objects.insert(NSDate(), at: 0)
        let indexPath = IndexPath(row: 0, section: 0)   //Itemを挿入する箇所を指定
        tableView.insertRows(at: [indexPath], with: .automatic)
    }
    
}

(注) Extensionを使って役割毎に切り分けてます。
(1) → viewDidLoad内でNavigation BarにItem(ボタン)をセットするメソッド setNavigationItem() ((2)で定義します)をコール。

(2) → Navigation BarにItem(ボタン)をセットするメソッド setNavigationItem() が書かれたところ。 やってることは以下の二つ。

  • Addボタンを定義。付帯動作(Selector)としてリストを管理してるオブジェクトに新しい項目を追加するメソッド insertNewObject()((3)で定義します)を指定。
  • Navigation Barの右側にAddボタンを追加。

(3) → リストを管理しているオブジェクトに新しい項目を追加するメソッド insertNewObject() が書かれたところ。リストを管理してるオブジェクトはクラスの頭でobjectとして定義済み。やってることは以下の二つ。

  • objectに項目を追加 objects.insert(NSDate(), at: 0)
  • TableViewのリストに項目を追加するUI処理tableView.insertRows(at: [indexPath], with: .automatic)

2-3. Delegateメソッドの実装 - その1 numberOfSectionsとtableView:titleForHeaderInSection:

TableViewControllerって色々Delegateメソッドを実装してあげないとちゃんと動いてくれないどころか、ビルドすら通してくれないことがあるので、一つ一つ丁寧に実装していく必要があります。今回は備忘録的な意味合いも兼ねてるので、必須でない部分についても少し書き残しておきたいと思ってます。
まずはnumberOfSectionsとtitleForHeaderInSectionから。

    //ListのSection数を返す 
    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 2
    }
    
    //Sectionのタイトル(※このメソッドを書かない場合にはSection帯が表示されない)
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        let sectionTitle: String = "\(section) section"
        return sectionTitle
    }

numberOfSectionsでListのSection数が決められます。そしてtableView:titleForHeaderInSection:でそのSectionのタイトルを決定します。因みにtableView:titleForHeaderInSection:を書かないとSection帯は表示されません。
Sectionというのは以下の画像にあるようなリスト項目上部の帯(今回は「数値 + section」を名称として表示しています)のことです。

これらは必須のDelegateメソッドではありませんが、Sectionというもの自体がこの先の必須Delegateメソッドを説明する上で考慮していた方が良いと思われるため、あえて一番最初に説明しました。

2-4. Delegateメソッドの実装 - その2 tableView:numberOfRowsInSection:

    //TableViewのセル数(Section毎)
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        if section != 0 {
            return 0
        }
        return objects.count
    }

必須メソッド。Section毎のRow(行)の数を返します。つまりリストの項目数です。今回はリストを管理してるオブジェクトであるobjectのメンバー数がそのまま0 sectionの項目数になります。
if section != 0 { return 0 } の記載は0 section以外の項目数は0であることを意味してます。2-3のnumberOfSectionsとtableView:titleForHeaderInSection:を設定しなければ必要ではありませんが、もし設定している場合には、何かしらかの矛盾なき数値を設定してあげないと2枚目のStoryboardに移動したときに落ちちゃいますので注意です。

2-5. Delegateメソッドの実装 - その3 tableView:cellForRowAt:

    //各セルの要素を設定する
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.section != 0 {
            fatalError("section\(indexPath.section) is invalid")
        }
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let object = objects[indexPath.row] as! NSDate
        cell.textLabel!.text = object.description
        return cell
    }

2-4でSection毎のRowの数は指定されていますが、ここではそのRow毎にどのようなUITableViewCellを表示するかというのを指定しています。引き続き必須メソッドです。
引数にあるindexPathというのは今回内容を表示するTableView上のポジション。TableViewを表示する時、2-4で指定したRowの数だけループを回してindexPath毎に表示するCellの内容を指定する、というイメージ(と私は理解しています)。
ここで注意すべきは let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) の箇所。ここではStoryboardのTableViewCellとコードを紐づけてます。一旦Storyboardを開いてTableView内のCellを選択します。そしてAttributes inspector内のTable View Cell - Identifier に名前を指定します。今回はCellと指定しました。この名前をtableView.dequeueReusableCellの引数withIdentifierに指定すれば紐付け完了です。

2-3でnumberOfSectionsとtableView:titleForHeaderInSection:を設定してますので、2-4と同じくif section != 0 { …. } みたいな処理をしてあげてないと正常に動作しませんので注意。

2-5. Editボタンを追加

// MARK: - extension ListViewController for Private Method
extension ListViewController {
    /**
     Navigation BarのボタンItemをセットする
    */
    private func setNavigationItem() {
        
        //Navigation Bar 左側
        //Editボタン追加
        self.navigationItem.leftBarButtonItem = self.editButtonItem
        
        
        //Navigation Bar 左側
        //Addボタン追加
        let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
        navigationItem.rightBarButtonItem = addButton
    }
    
}

EditボタンはUIKitで予め用意してくれているものを使うのがお手軽です。
self.navigationItem.leftBarButtonItem = self.editButtonItem でNavigation Barの左側に配置されます。

2-6. Delegateメソッドの実装 - その4 setEditing

setEditing()の説明の前に。Editボタンを押したら以下の画像にあるような一連の動作をさせたいです。よくある動作ですね。
f:id:anthrgrnwrld:20200210184231p:plain:w300

setEditing() は、Editボタンを押したら呼ばれるメソッドです。Editボタンを押した時に特別な動作をさせたい時はここに書きましょう。編集モードに設定する場合には tableView.isEditing = editing を書いておくこと。そして、このメソッドを書かない場合には自動的に編集モードになります。

    //Editボタンが押されると呼ばれる(※このメソッドを書かない場合にはEditボタンを押すと自動的に編集モードになる)
    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        tableView.isEditing = editing   //編集モードに設定
    }

2-7. Delegateメソッドの実装 - その5 tableView:canEditRowAt:

    //削除可能なセルのindexPathを指定(※このメソッドを書かない場合には全てのCellが削除可能)
    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return true     //すべてのCellを削除可能
    }

削除可能なRowを数値で指定します。0番目だけ編集可能にしたければ if indexPath.row == 0 { return true} とすればよいです。でも、、まぁ普通はすべてのRowで編集可能にしますよね。

2-8. Delegateメソッドの実装 - その6 tableView:editingStyle:forRowAt:

削除された時の処理を実行します。何も処理しないとUIの表示とリストを管理してるオブジェクトobjectの内容とに矛盾が生じちゃうので、ユーザーに削除と指定された項目はobjectからも削除しなければいけません。

    //削除された時の処理を実装する
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            // Delete the row from the data source
            objects.remove(at: indexPath.row)                   //先にデータの更新
            tableView.deleteRows(at: [indexPath], with: .fade)  //それからテーブルの更新 (この順番を間違えると落ちる)
        } else if editingStyle == .insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
        }
    }

2-9. Delegateメソッドの実装 - その7 tableView:canMoveRowAt:

2-7のsetEditingの並び替え機能版です。並べ替え可能なRowを指定しますが、、これも普通は全てのRowを指定しますよね。

    //並び替え可能なセルの indexPath を指定(※このメソッドを書かない場合には全てのCellが並び替え不可能)
    override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true    //すべてのCellを並び替え可能にする
    }

2-10. Delegateメソッドの実装 - その8 tableView:moveRowAt:to:

並び替えされた際に実行されるメソッドです。2-9と同じように、ここでも何も処理しないとUIの表示とリストを管理してるオブジェクトobjectの内容とに矛盾が生じちゃうので、objectの内容についてもちゃんと並び替えしないとです。

    //並び替え処理
    override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
        //データの順番を整える
        let targetTitle = objects[fromIndexPath.row]
        objects.remove(at: fromIndexPath.row)
        objects.insert(targetTitle, at: to.row)
    }

fromIndexPathが移動前のIndexPath、toが移動後のIndexPathになります。ここで取得した各IndexPathから実データであるobjectの並び替え処理を行います。

3. DetailViewControllerの実装

3-1. 新規Swiftファイルを作成

2-1と同じように新規Swiftファイルを作成します。今回は普通のUIViewControllerを継承すればよいです。名前は「DetiailViewController」と名付けました。
f:id:anthrgrnwrld:20200210184946p:plain:w300

3-2. ラベルのアウトレットを作成しStoryboardと紐付ける

1-3でで作成した3枚目のStoryboardのUILabelからControlキーを押しなからDetailViewControllerにドラッグアンドドロップします。そうするとDetailViewControllerにIBOutletが作成されます。名前は「detailDescriptionLabel」としました。

class DetailViewController: UIViewController {

    @IBOutlet weak var detailDescriptionLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
}

3-3. ラベルの表示内容を更新する仕組みを用意しておく

今回は他のViewController(2枚目のListViewController)からラベルの内容を更新する必要があるので、そのインターフェースを用意しておく必要があります。今回は detailItem という名前で用意しました。

class DetailViewController: UIViewController {

    @IBOutlet weak var detailDescriptionLabel: UILabel!
    
    var detailItem: NSDate? 
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
}

他のViewControllerからdetailItemに値を入れるだけではラベルの更新はされませんので、更新される仕組みの実装も必要です。以下のコードのようにラベルをdetailItem内容に従って更新する configureView() メソッドを用意します。それをviewDidLoadで呼んであげることで、表示の更新がされる仕組みにしました。

class DetailViewController: UIViewController {

    @IBOutlet weak var detailDescriptionLabel: UILabel!
    
    var detailItem: NSDate? 
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureView()
    }
    
    func configureView() {
        // Update the user interface for the detail item.
        if let detail = detailItem {
            //遷移前に別Controllerでセットされた時にはdetailDescriptionLabelがnullの為、
            //viewDidLoadにて configureView()を実行する必要がある
            if let label = detailDescriptionLabel {
                label.text = detail.description
            }
        }
    }
    
}

4. ListViewControllerからDetilViewControllerへの表示内容の受け渡し

4−1. Segueにidを付ける

1−5で作成したStoryboard上のSegueにidをつけます。idをつけることによって、遷移時のタイミングにどの遷移方法で移動したかを把握することができるようになります。今回はそのタイミングでListViewで表示されているCellの内容を3枚目のDetilViewControllerへ渡さなければなりません。
Idのつけ方ですが、まず2枚目と3枚目のViewControllerの間の矢印を選択して下さい。
f:id:anthrgrnwrld:20200210191510p:plain

そしてAttributes inspectorを選択し、idを指定します。今回は「showDetail」としました。
f:id:anthrgrnwrld:20200210191647p:plain

4-2. 遷移時の処理を作成

PrepareはSegueが動作することをViewControllerに通知するメソッド。そこでSegueのidが先ほど指定したshowDetailの時に限って値渡しが処理されるようにします。
let controller = segue.destination as! DetailViewController でDetailViewControllerのインスタンスを作成し、そこから3−3で作成したインターフェイスとなる変数 detailItem に渡したい値を入れます。
渡したい値については、tableView.indexPathForSelectedRow で選択されたRowが取得出来ますので、そのRowに対応したものをリストを管理してるオブジェクトobjectから取得します。

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
        if segue.identifier == "showDetail"{
            if let indexPath = tableView.indexPathForSelectedRow {
                let object = objects[indexPath.row] as! NSDate
                let controller = segue.destination as! DetailViewController
                controller.detailItem = object
            }
        }
    }

5. 完成

ビルドして動かしてみましょう。
しかしながらここまででビルドできるタイミングはいくつかあります。2-3まで完成以降は組んでは動きを確認という方法が勉強になると思いますのでビ、適宜実行してみるのがいいのではないでしょうか。

まとめ

リストを使ったアプリができました!(色々考慮できてない部分などはあるとは思いますが。。。)
この形を基本としてデータベース対応などしていけば立派なアプリが出来上がるのではと思います。