34439


cell内のTextFieldの入力が完了した後、新しいcellの追加がしたいです。  
iPhoneのリマインダーアプリのように、入力完了後、キーボードの改行ボタンを押すと、  
新しくcellが追加されるという形が理想です。

よろしくお願いします。

* * *
[code]

     //デリゲートメソッド
    func textFieldShouldReturn(_ textField: UITextField) -> Bool
    {
        //キーボードを閉じる。
        textField.resignFirstResponder()
        return true
    }
    
[/code]

今は、returnキーを押すと、キーボードが閉じるようにしてあります。  
このキーボード閉じる動作にプラスして新しくセルを追加できるようにしたいです。  
上のコードにどういうコードを書き足せば実行できるのかがわかりません。


CC BY-SA 3.0
2017-05-03T03:24:55.743
34427
2017-05-03T15:54:51.800
2017-05-03T12:24:56.793
22835
22835
question
swift uitableview tableview
cell内のTextFieldの入力が完了した時、新しいcellの追加がしたい


テーブルに新しい行を挿入する方法を説明します。

[UITableView -
insertRows(at:with:)](https://developer.apple.com/reference/uikit/uitableview/1614879-insertrows)

`UITableView`クラスの、このメソッドを使用するのですが、使い方に二つのポイントがあります。ひとつは、挿入時のアニメーションをするために、`beginUpdates()`と、`endUpdates()`で挟んでやるということ。もうひとつは、テーブルに表示する元データに対する新データの挿入も、同時に行うということです。  
以下、サンプルコードを示します。コード中、`tableView`は、プロパティとして宣言したテーブルビュー自身、`tableData`は、テーブルに表示する元データ。文字列`String`の配列`[String]`とします。また、`UITextField`を貼り付けたセルは、`UITableViewCell`のサブクラス`CustomCell`としており、`CustomCell`は、プロパティ`textField`を持っています。

[code]

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        tableView.beginUpdates() // アニメーション開始。
        // テーブルの最終行を、IndexPathとして取得。
        let lastRowIndex = tableView.numberOfRows(inSection: 0)
        let indexPath = IndexPath(row: lastRowIndex, section: 0)
        // 行を最終行の次に挿入。
        tableView.insertRows(at: [indexPath], with: .automatic)
        // 元データにも挿入。
        tableData.insert("", at: lastRowIndex)
        tableView.endUpdates() // アニメーション終了。
        // 新しい行のテキストフィールドを、テキスト入力状態にする。
        if let cell = tableView.cellForRow(at: indexPath) as? CustomCell {
            cell.textField.becomeFirstResponder()
        }
    
        return true
    }
    
[/code]

* * *

リマインダー風のインターフェイスにするには、もうひと工夫、ふた工夫必要です。知恵を絞り出して、ユーザフレンドリーなインターフェイスを実現していってください。


CC BY-SA 3.0
2017-05-03T13:50:31.337
34439
2017-05-03T13:50:31.337
18540
34427
answer


ご質問内容のように、追加用のセルは常にテーブルビューの最後の行に表示される、と言う動作を実現したい場合、それ専用のちょっと特殊な書き方をした方がいい場合があります。

* * *

## 【前提事項】

「リマインダーアプリのように」以外のご指定が無いので、ここでは簡略化した以下のようなクラスがテーブルビュー内の各行に表示されるものとします。

[code]

    class Item {
        ///チェックされているかどうか
        var isChecked: Bool
        ///表示されるメインのテキスト(簡略化のためサブテキスト等は無いことにする)
        var text: String
        
        init(text: String, checked: Bool) {
            self.text = text
            self.isChecked = checked
        }
    }
    
[/code]

セルの定義は、2種類(通常セルと追加セル)をPrototype
Cellに登録し、それぞれにカスタムクラス(`MyNormalCell`と`MyAddingCell`)を設定しているものとします。Identifierもクラス名と同じにしてあります。

[![プロトタイプセル](https://i.stack.imgur.com/W3BYU.jpg)](https://i.stack.imgur.com/W3BYU.jpg)

(`UITextField`を配置したり、そのdelegateを`ViewController`に接続したり、といったことは確実にできているものとします。)

このセルを使って、先ほどの`Item`の配列をテーブルビューに表示する場合、追加セルの表示を気にしなければ`ViewController`クラスの記述はこんな感じになるでしょう。

[code]

    class ViewController2: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
        
        @IBOutlet weak var tableView: UITableView!
        
        var dataItems: [Item] = [
            Item(text: "A", checked: false),
            Item(text: "B", checked: true),
            Item(text: "C", checked: false)
        ]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            //UITableViewのdelgate, dataSourceの接続はStoryboardエディター上でも設定できるがわかりにくいのでこちらで
            tableView.delegate = self
            tableView.dataSource = self
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataItems.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "MyNormalCell", for: indexPath) as! MyNormalCell
            cell.checkButton.isSelected = dataItems[indexPath.row].isChecked
            //textField.tagの値からindexPath.rowを取得できるようにしておく
            cell.textField.tag = indexPath.row
            cell.textField.text = dataItems[indexPath.row].text
            return cell
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            //...
            return true
        }
        
        //...
    }
    
[/code]

上のコードをそのまま実行すると、こんな感じになります。

[![追加セルなし](https://i.stack.imgur.com/JJEg0.png)](https://i.stack.imgur.com/JJEg0.png)

* * *

## 【ここから本番】

さて、上記のようなコードでテーブルビューの最後に追加用のセルを表示するにはどうしたら良いかと言うと、ポイントは:

  * 行数(numberOfRows)は、(データの個数)+(追加用セルの個数(=1))
  * IndexPathが追加用セルを指している場合、MyNormalCellではなく、MyAddingCellを返す

の2点ということになります。

追加用セルが必要であることを示すプロパティ`hasAddingItem`を追加し、`UITableViewDataSource`の2つのメソッドを書き直してやります。

[code]

    var hasAddingItem: Bool = false
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //hasAddingItemがtrueの場合には、データ数に1足した数を行数とする
        return dataItems.count + (hasAddingItem ? 1 : 0)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if hasAddingItem && indexPath.row == dataItems.count {
            let addingCell = tableView.dequeueReusableCell(withIdentifier: "MyAddingCell", for: indexPath) as! MyAddingCell
            //indexPath.row == dataItems.count、つまりdataItemsの最終要素のindexより1大きな値になっている
            addingCell.textField.tag = indexPath.row
            addingCell.textField.text = ""
            return addingCell
        } else {
            let cell = tableView.dequeueReusableCell(withIdentifier: "MyNormalCell", for: indexPath) as! MyNormalCell
            cell.checkButton.isSelected = dataItems[indexPath.row].isChecked
            //textField.tagの値からindexPath.rowを取得できるようにしておく
            cell.textField.tag = indexPath.row
            cell.textField.text = dataItems[indexPath.row].text
            return cell
        }
    }
    
[/code]

この修正を施したコードをそのまま実行しても、先ほどと実行結果は変わりませんが、追加したプロパティ`hasAddingItem`の初期値を`true`にして実行してやると、こんな結果になります。

[![画像の説明をここに入力](https://i.stack.imgur.com/PGA4Y.png)](https://i.stack.imgur.com/PGA4Y.png)

と言うわけで、`hasAddingItem`の初期値としては`false`にしておいて、適切な契機でその値を`true`にしてやれば、追加用セルの表示が行える、と言うことになります。

但し、

  * セルが増えたり減ったりするのはテーブルビュー用データが書き換わったことになるので、`realoadData()`が必要
  * 普通は追加されたセル上の`UITextField`にフォーカスが移るはず

といったことも考えておかないといけません。

ざっくり上の2点だけをコードに反映しようと思うと、`textFieldShouldReturn(_:)`が例えばこんな感じになります。

[code]

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        //tagの値で通常セル上のtextFieldか追加セル用のtextFieldかを区別する
        if textField.tag == dataItems.count {
            //MyAddingCell上のTextFieldの場合の処理
            //dataItemsに追加用セルで編集されていたデータを追加
            dataItems.append(Item(text: textField.text ?? "", checked: false))
        } else {
            //MyNormalCell上のTextFieldの場合の処理
            //追加セルの表示を有りにする
            hasAddingItem = true
        }
        tableView.reloadData()
        //追加セルのIndexPathを作成
        let indexPath = IndexPath(row: dataItems.count, section: 0)
        let addingCell = tableView.cellForRow(at: indexPath) as! MyAddingCell
        addingCell.textField.becomeFirstResponder()
        tableView.scrollToRow(at: indexPath, at: .none, animated: true)
        return true
    }
    
[/code]

実際のアプリとして考えると、上記のコードだけではまだまだ変な動きをする部分があるのですが、それはまた別件ということで。

ご質問中にコードがほとんど示されていないので、あなたの現在のコードに当てはめるためにどの程度修正が必要かはわからないのですが、考え方はそのまま使えるはずですので、お試しいただければと思います。


CC BY-SA 3.0
2017-05-03T15:54:51.800
34443
2017-05-03T15:54:51.800
2020-06-17T08:14:45.997
-1
13972
34427
answer