

## 実現したいこと＆現状

segmentedcontrollにて2つのcaseに分かれています。  
それぞれに変数を設けtodosで登録されたデータをそれぞれ使っております。  
cellのSwipe機能にShareを押すとcase1からcase2にcellが移ります。

Cellのdeleteをしたいです。

## エラーと問題点

cellのdelete機能はプログラミング上正常に作動しているのですが、deleteを押すと

[code]

    'Invalid update: invalid number of rows in section 0. 
    The number of rows contained in an existing section after the update (4) must be equal to the number of rows contained in that section before the update (4),
    plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out)
    
[/code]

と表示されます。またビルドするとcellは消えているのですがcrashしてしまうので格納されているデータと表示したいデータが共通ではないのだと思います。  
改善策はありますか？

## ソースコード

[code]

    import UIKit
    
    class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
        
        //registered cell
        var todos:[Item] = []
        //to show in First case
        var allTodo:[Item] = []
        //to show in second case
        var selectedTodo:[Item] = []
        
        @IBOutlet weak var Table:UITableView!
    
        @IBAction func segmentselected(_ sender: UISegmentedControl) {
            
            switch sender.selectedSegmentIndex {
                
            case 0:
                todos = allTodo
            case 1:
                todos = selectedTodo
            default:
                fatalError("case でカバーできていません")
            }
            Table.reloadData()
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view
            Table.delegate = self
            Table.dataSource = self
        
            //UD読み込み
            if let data = UserDefaults.standard.data(forKey: "todoList"){
                self.allTodo = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as!
                    [Item]
            }
            
            if let data2 = UserDefaults.standard.data(forKey: "todoShare"){
                self.selectedTodo = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data2) as!
                    [Item]
            }
            
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return todos.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            let cell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            let item = todos[indexPath.row]
            cell.textLabel!.text = item.title
            return cell
        }
        
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return view.frame.size.height/10
        }
        
        @IBAction func addNewTodo(_sender: Any){
            var textField = UITextField()
            
            let alert = UIAlertController(title: "新しいTodoを追加", message: "", preferredStyle: .alert)
            
            let action = UIAlertAction(title: "リストに追加", style: .default) { (action) in
                
                let newItem:Item = Item(title: textField.text!)
                print("追加されました")
                
                self.allTodo.append(newItem)
                
                //UD保存
                let data = try! NSKeyedArchiver.archivedData(withRootObject: self.allTodo, requiringSecureCoding: false)
                UserDefaults.standard.set(data, forKey: "todoList")
                
                //コピー
                self.todos = self.allTodo
                
                self.Table.reloadData()
            }
            
            alert.addTextField { (alertTextField) in
    
                alertTextField.placeholder = "新しいTodo"
                textField = alertTextField
            }
                
            alert.addAction(action)
            present(alert, animated: true,completion: nil)
                
        }
        
        //swipe action
        func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    
        // シェアのアクションを設定する
            let shareAction = UIContextualAction(style: .normal  , title: "share") {
            (ctxAction, view, completionHandler) in
             print("シェアを実行する")
                
                //add cell to second case
                self.selectedTodo.append(self.allTodo[indexPath.row])
                let data2 = try! NSKeyedArchiver.archivedData(withRootObject: self.selectedTodo, requiringSecureCoding: false)
                UserDefaults.standard.set(data2, forKey: "todoShare")
                
                completionHandler(true)
                self.Table.reloadData()
        }
            
        // シェアボタンのデザインを設定する
        let shareImage = UIImage(systemName: "square.and.arrow.up")?.withTintColor(UIColor.white, renderingMode: .alwaysTemplate)
        shareAction.image = shareImage
        shareAction.backgroundColor = UIColor(red: 0/255, green: 125/255, blue: 255/255, alpha: 1)
    
        // 削除のアクションを設定する
        let deleteAction = UIContextualAction(style: .destructive, title:"delete") {
            (ctxAction, view, completionHandler) in
            self.allTodo.remove(at: indexPath.row)
            self.selectedTodo.remove(at: indexPath.row)
            
            //削除した結果も保存
            let data = try! NSKeyedArchiver.archivedData(withRootObject: self.allTodo, requiringSecureCoding: false)
            UserDefaults.standard.set(data, forKey: "todoList")
            let data2 = try! NSKeyedArchiver.archivedData(withRootObject: self.selectedTodo, requiringSecureCoding: false)
            UserDefaults.standard.set(data2, forKey: "todoShare")
            
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            //コピー
            self.todos = self.allTodo
            completionHandler(true)
            tableView.reloadData()
        }
        
        // スワイプでの削除を無効化して設定する
        let swipeAction = UISwipeActionsConfiguration(actions:[deleteAction, shareAction])
        swipeAction.performsFirstActionWithFullSwipe = false
        
        return swipeAction
        }
    }
    
    class Item: NSObject,NSCoding{
        
        var title:String
        
        init(title:String) {
            self.title = title
        }
        
        func encode(with coder: NSCoder) {
            coder.encode(self.title,forKey: "title")
        }
        
        required init?(coder: NSCoder) {
            self.title = coder.decodeObject(forKey: "title") as! String
        }
        
    }
    
[/code]


CC BY-SA 4.0
2020-07-09T14:49:13.150
68448
2020-07-09T18:42:48.607
41026
question
swift ios xcode
TableView delete機能エラーについて。


まず最初に。

「cellのdelete機能はプログラミング上正常に作動している」と書いておきながら、「deleteを押すと…crashしてしまう」と書かれています。crashするものを「正常に作動」とは言いませんから、矛盾していますよね?

「cellをスワイプしたときにdeleteを表示するところまでは出来ている」と言いたかったのでしょうか?
この辺の表現の違いで、いったいどの部分で何が起こっているのかの判断が異なってくることになるので、ご質問を書かれるときには細心の注意を払うようにお願いします。

(今回は必要なコードを全て提示していただいたので、コードを熟読すれば何とか意味が掴めましたが、逆に言うと「熟読しないと回答が書けない」と言うことでもあります。)

また、あなたのクラス内で`Table`なんてプロパティは、型名でもないのに大文字で始まっています。Swiftでは、「大文字で始めるのは型名だけ」と言うルールは非常によく守られていますが、それを無視することでコードが極めて読みにくいものになってしまっています。動作確認まで進んでいるコードのOutlet名を変更すると結構苦労をすることになるので、今からの変更はお勧めできませんが、最初から基本的なコーディングルールを守るようにして下さい。

* * *

さて、「格納されているデータと表示したいデータが共通ではないのだと思います」とご自身で書かれていますが、あまりにもざっくりとしてはいますが、ほぼ正解だと言えるでしょう。

`tableView(_:numberOfRowsInSection:)`の定義から、あなたの画面の`UITableView`は、行数(number of
rows)を`todos`の要素数から取得します。

`UITableView`は、`deleteRows(at:with:)`が呼ばれると、「最後に取得した行数」、「削除した行数」(`at:`に指定した`IndexPath`の個数)、「現在の行数」を比べて、

「最後に取得した行数」 - 「削除した行数」 = 「現在の行数」

になっていないと、「やっていることが矛盾している」として、アプリをクラッシュさせます。

「最後に取得した行数」は以前の`todos.count`、「現在の行数」は現在の`todos.count`として取得されますので、

## `deleteRows(at:with:)`で「1行削除」を知らせる前に、`todos`を1要素減らしておかないといけない

と言うことです。

あなたのコードでは、`deleteRows(at:with:)`を呼ぶ前に、
**`allTodo`と`selectedTodo`の2つの配列から1要素削除しています**(*)が、`todos`からはデータを削除していません。そこで`UITableView`は「やっていることが矛盾している」と言っているわけです。

* * *

で、コードをどう修正するかを考えるときに、上の(*)でやっていることがおかしいことに気づきます。

`allTodo`と`selectedTodo`とは中身が異なるのが普通なのに、どちらも`remove(at:
indexPath.row)`と「現在の`todos`内でのインデックス」で要素を削除しようとしています。すると表示されていない方の配列からは、関係ない要素が削除されたり、配列の範囲外を削除しようとしてクラッシュしたりします。

できるだけ修正箇所が広がらないように直すことを考えると、例えば以下のようになるでしょうか。

[code]

        // 削除のアクションを設定する
        let deleteAction = UIContextualAction(style: .destructive, title:"delete") {
            (ctxAction, view, completionHandler) in
            
            //`todos`から要素を削除し、削除した要素を保持しておく
            let deletedItem = self.todos.remove(at: indexPath.row)
            //`allTodo`と`selectedTodo`からの削除はインデックスではなく、`deletedItem`と言う要素で指定する
            self.allTodo.removeAll {$0 === deletedItem}
            self.selectedTodo.removeAll {$0 === deletedItem}
            
            //削除した結果も保存
            //...(この部分はとりあえず変更なし)
            
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            //コピー
            self.todos = self.allTodo //<-??? `UISegmentedControl`も見ずにいつでも`allTodo`で大丈夫?
            completionHandler(true)
            tableView.reloadData()
        }
    
[/code]

最後の`???`をつけた行、意味が理解できなかったので、一旦そのままにしたのですが、delete後は表示が必ず`allTodo`側に切り替わるので良いのでしょうか?その場合には`UISegmentedControl`の`selectedSegmentIndex`も書き換えなくて大丈夫でしょうか?

もし「削除」を`todos`に反映したいだけであれば、上記のコードではもう更新されているので、`self.todos =
self.allTodo`と`tableView.reloadData()`は不要になります。

[code]

        // 削除のアクションを設定する
        let deleteAction = UIContextualAction(style: .destructive, title:"delete") {
            (ctxAction, view, completionHandler) in
            
            //`todos`から要素を削除し、削除した要素を保持しておく
            let deletedItem = self.todos.remove(at: indexPath.row)
            //`allTodo`と`selectedTodo`からの削除はインデックスではなく、`deletedItem`と言う要素で指定する
            self.allTodo.removeAll {$0 === deletedItem}
            self.selectedTodo.removeAll {$0 === deletedItem}
            
            //削除した結果も保存
            //...(この部分はとりあえず変更なし)
            
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            completionHandler(true)
        }
    
[/code]

* * *

あなたのコードでは、`todos`, `allTodo`,
`selectedTodo`と言う3つの配列と`UISegmentedControl`の`selectedSegmentIndex`を常に整合性が取れるように更新してやらないとうまく動きません。そのため他にもいろいろ手直ししないと正しく動作しない可能性が高いですが、とりあえずdeleteについては、上の修正で動くようになるのではないかと思います。お試し下さい。


CC BY-SA 4.0
2020-07-09T18:42:48.607
68453
2020-07-09T18:42:48.607
13972
68448
answer