質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

2644閲覧

Realmで保存したTableViewのセルのデータをSwipeActionで削除したいです。

Ytan

総合スコア39

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

1クリップ

投稿2020/09/10 12:22

現状
registeredMoviesに他のViewControllerでrealm.writeで保存された
Realmから取り出した全てのデータがあり、
テーブルに表示するデータであるdataSourceという二つの変数を用意しました。
updateRegisteredMovieというメソッドでRealmに保存したid:Intで
データ(インデックス)を識別して、isWatedがtrueかfalseかによって
segmentedControllのcase0かcase1にデータを表示しているか判断しています。

疑問
UISwipeactionでupdateRegisteredMovieをshareActionでdeleteRegisteredMovie
deleteActionで行いたいです。
updateRegisteredMovieの方は他の方から教わりこの方法でデータの更新できるのですが、
deleteRegisteredMovieの方は自分で書いてみたのですが、このまま実行すると

Object has been deleted or invalidated

とエラーが出てしまいます。
Realmで一つのCellを削除する場合どのように行えば良いですか?

補足
cellをスワイプしたときにShare,deleteを表示するところまでは出来ています。

let selectedRegisterdMovieID = self.dataSorce[indexPath.row].id

はTableViewのindexとRealmのindexのデータの個数が違う場合に更新対象と違うデータがとれてしまう可能性を防ぐために
Realmに保存したidで削除/更新するindexを判断しています。

ソースコード

import UIKit import RealmSwift class ListViewController: UIViewController { @IBOutlet weak var table:UITableView! @IBOutlet weak var segmentedControl:UISegmentedControl! private var registeredMovies = [RegisteredMovie]() private var dataSorce: [RegisteredMovie] { switch segmentedControl.selectedSegmentIndex { case 0: return registeredMovies.filter { !$0.isWatched} case 1: return registeredMovies.filter { $0.isWatched} default: fatalError("Unknown case.") } } override func viewDidLoad() { super.viewDidLoad() self.setupTable() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) loadRegisteredMovies() } @IBAction func segmentSelected(_ sender: UISegmentedControl) { table.reloadData() } } extension ListViewController { private func setupTable() { table.delegate = self table.dataSource = self table.register(ListTableViewCell.nib(), forCellReuseIdentifier: ListTableViewCell.identifier) } } extension ListViewController { private func loadRegisteredMovies() { do { let realm = try Realm() let storedRegisteredMovies = Array(realm.objects(RegisteredMovie.self)) registeredMovies = storedRegisteredMovies table.reloadData() } catch { print(error) } } private func updateRegisteredMovie(of id: Int, isWatched: Bool) { guard let index = registeredMovies.firstIndex(where: { $0.id == id }) else { return } do { let realm = try Realm() try realm.write { registeredMovies[index].isWatched = isWatched } table.reloadData() } catch { print(error) } } //データを削除するメソッド func deleteRegisteredMovie(of id: Int) { guard let index = registeredMovies.firstIndex(where: { $0.id == id }) else { return } do { let realm = try Realm() try realm.write { realm.delete(registeredMovies[index]) } self.table.reloadData() } catch { print(error) } } //swipe action func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { // シェアのアクションを設定する let shareAction = UIContextualAction(style: .normal , title: "完了") { (ctxAction, view, completionHandler) in //データをcase1に更新 let selectedRegisterdMovieID = self.dataSorce[indexPath.row].id let isWatched = self.segmentedControl.selectedSegmentIndex == 0 self.updateRegisteredMovie(of: selectedRegisterdMovieID, isWatched: isWatched) completionHandler(true) self.table.reloadData() } // シェアボタンのデザインを設定する let shareImage = UIImage(systemName: "arrowshape.turn.up.right.fill")?.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:"削除") { (ctxAction, view, completionHandler) in //疑問点 let selectedDeleteMovieID = self.dataSorce[indexPath.row].id self.deleteRegisteredMovie(of: selectedDeleteMovieID) tableView.deleteRows(at: [indexPath], with: .automatic) completionHandler(true) } // スワイプでの削除を無効化して設定する let swipeAction = UISwipeActionsConfiguration(actions:[deleteAction, shareAction]) swipeAction.performsFirstActionWithFullSwipe = false return swipeAction } } extension ListViewController: UITableViewDataSource { --以下省略

Realmのクラスには以下が含まれています。

import Foundation import RealmSwift class RegisteredMovie: Object { @objc dynamic var id: Int = 0 @objc dynamic var title: String = "" @objc dynamic var release_date: String = "" @objc dynamic var poster_path: String = "" @objc dynamic var isWatched: Bool = false }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

TsukubaDepot

2020/09/10 23:43

Realm は日常的に使っていないので、とりあえずこちらでコメントを書きますので、試していただければと思います。 こちらで簡単な例を作った上での推測ですが、実際にエラーが出ているのは、private var dataSorce の getter ではないでしょうか。 loadRegisteredMovies() で let storedRegisteredMovies = Array(realm.objects(RegisteredMovie.self)) registeredMovies = storedRegisteredMovies とやっていますが、Relms のオブジェクトと registeredMovies はここで一回しか紐づいていないため、削除する処理で Realm のオブジェクトを削除しても registerdMovie には正確に反映されていないと思います。 たとえば、 var registeredMovies: [RegisteredMovie] { let realm = try! Realm() let res = realm.objects(RegisteredMovie.self) return Array(res) } みたいにすると、動くのではないでしょうか(もちろん、実際にはtry のエラー処理も必要ですが)。
Ytan

2020/09/11 01:14

つまりdeleteRegisteredMovieのrealm.delete(registeredMovies[index])は必要ではないということですか?
TsukubaDepot

2020/09/11 01:16

いや、そこは必要なのですが、その操作をした後 registerdMoviews がRealmのデータベースの変更を反映していないのが原因だろう、ということです。
Ytan

2020/09/11 15:15

返信が遅れてしまいすみません。 なるほどそういうことですか。 変更を施してみたところCannot assign to property: 'registeredMovies' is a get-only property エラーがloadRegisteredMoviesの方で生じたのですが、こちらは https://fuuno.net/swift/computed/computed.html このリンクのようにcomputedプロパティにget(getter?)のみが書かれているため setを設けなければならないということですか?
TsukubaDepot

2020/09/12 05:06

全てを見ていなかったため、データ取得時のみに着目していましたが、registeredMovies のプロパティに直接アクセスする場所もあるようですね。そうなると setter も必要ですが、処理がややこしくなりそうな気がします。 おそらく、根本的な原因は、realm.objects で返される型を Array にしていることではないかと思うのですが、Arrayにするのは本当に必要なときだけにして、それ以外はしかるべき型にしなければならないと思いますが、如何でしょうか。 Realm のライブアップデートという特性を考えると、Array にしてしまうとトラブルの原因になりそうな気がします。 https://realm.io/jp/docs/swift/latest/#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E8%87%AA%E5%8B%95%E6%9B%B4%E6%96%B0%E3%83%A9%E3%82%A4%E3%83%96%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88 あと、Realm に保存したオブジェクトは、直接更新するのではなく、realm.add で更新しなければならないと読んだ気がするのですが、それはいかがでしょうか(こちらはあまり自信がありません)。
Ytan

2020/09/12 14:58

自分もrealmを初めて使用するので何とも言えませんが、違う配列で更新を行ってから本来のデータに反映できると見ました。 改めてRealmの概要を確認してみたいと思います。
guest

回答1

0

ベストアンサー

いくつか試してみましたが、現状のコードをなるべく変えずに問題を解決するには、realm.objets() が返す型である Results<Element>Array<> にせず、そのまま使うことだと思います。

型変換さえしなければ、

Swift

1 //private var registeredMovies = [RegisteredMovie]() 2 private var registeredMovies: Results<RegisteredMovie>!

という具合に宣言し、

Swift

1 private func loadRegisteredMovies() { 2 do { 3 let realm = try Realm() 4// let storedRegisteredMovies = Array(realm.objects(RegisteredMovie.self)) 5// registeredMovies = storedRegisteredMovies 6 7 registeredMovies = realm.objects(RegisteredMovie.self) 8 9 table.reloadData() 10 } catch { 11 print(error) 12 } 13 }

という具合に型変換せずそのまま代入すればいいかと思います。

ただ、セルを削除する際アニメーションさせるのであれば、

Swift

1 func deleteRegisteredMovie(of id: Int) { 2 3 guard let index = registeredMovies.firstIndex(where: { $0.id == id }) else { 4 return 5 } 6 do { 7 let realm = try Realm() 8 try realm.write { 9 realm.delete(registeredMovies[index]) 10 } 11 12 // MARK: deleteRows() をつかうのでれば、reloadData() は呼び出さない 13 //self.table.reloadData() 14 } catch { 15 print(error) 16 } 17 }

上記の reloadData() を実行してしまうと辻褄が合わなくなってしまいますので、コメントアウトしたほうがいいかと思います(私が追試したときにはここで実行時エラーが起きました)。

なお、より安全にするには

Swift

1 private var registeredMovies: Results<RegisteredMovie>?

と、?のオプショナル型で宣言したほうが良いかと思いますが、その場合各所でオプショナルバインディングやメソッドチェーンへの書き換えが必要になるかと思いますので、進捗に合わせて作業していただければと思います。

参考までに、実証コード。

Swift

1import UIKit 2import RealmSwift 3 4class ListViewController: UIViewController { 5 6 @IBOutlet weak var table:UITableView! 7 @IBOutlet weak var segmentedControl:UISegmentedControl! 8 9 // MARK: - 10 //private var registeredMovies = [RegisteredMovie]() 11 private var registeredMovies: Results<RegisteredMovie>! 12 13 private var dataSorce: [RegisteredMovie] { 14 15 switch segmentedControl.selectedSegmentIndex { 16 case 0: 17 return registeredMovies.filter { !$0.isWatched} 18 case 1: 19 return registeredMovies.filter { $0.isWatched} 20 default: 21 fatalError("Unknown case.") 22 } 23 } 24 25 override func viewDidLoad() { 26 super.viewDidLoad() 27 28 // MARK: データ全削除 - 初期化テスト用 29// let realmURL = Realm.Configuration.defaultConfiguration.fileURL! 30// let realmURLs = [ 31// realmURL, 32// realmURL.appendingPathExtension("lock"), 33// realmURL.appendingPathExtension("note"), 34// realmURL.appendingPathExtension("management") 35// ] 36// for URL in realmURLs { 37// do { 38// try FileManager.default.removeItem(at: URL) 39// } catch { 40// // handle error 41// } 42// } 43 44 // MARK: ダミーデータの登録 - データがなければ登録する 45 let realm = try! Realm() 46 47 if realm.objects(RegisteredMovie.self).count == 0 { 48 let movies = [ 49 RegisteredMovie(value: ["title": "Star Wars I", "id": 1, "isWatched": false]), 50 RegisteredMovie(value: ["title": "Star Wars II", "id": 2, "isWatched": false]), 51 RegisteredMovie(value: ["title": "Star Wars III", "id": 3, "isWatched": false]), 52 RegisteredMovie(value: ["title": "Star Wars IV", "id": 4, "isWatched": false]), 53 RegisteredMovie(value: ["title": "Star Wars V", "id": 5, "isWatched": false]), 54 RegisteredMovie(value: ["title": "Star Wars VI", "id": 6, "isWatched": false]), 55 RegisteredMovie(value: ["title": "Star Wars VII", "id": 7, "isWatched": false]), 56 RegisteredMovie(value: ["title": "Star Wars VIII", "id": 8, "isWatched": false]), 57 RegisteredMovie(value: ["title": "Star Wars IX", "id": 9, "isWatched": false]) 58 ] 59 60 try! realm.write { 61 realm.add(movies) 62 } 63 } 64 65 self.setupTable() 66 } 67 68 override func viewWillAppear(_ animated: Bool) { 69 super.viewWillAppear(animated) 70 71 loadRegisteredMovies() 72 } 73 74 @IBAction func segmentSelected(_ sender: UISegmentedControl) { 75 table.reloadData() 76 } 77} 78 79 80extension ListViewController { 81 82 private func setupTable() { 83 table.delegate = self 84 table.dataSource = self 85 // MARK: - 標準のセルを登録 86 //table.register(ListTableViewCell.nib(), forCellReuseIdentifier: ListTableViewCell.identifier) 87 table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 88 } 89} 90 91extension ListViewController: UITableViewDelegate { 92 93 private func loadRegisteredMovies() { 94 do { 95 let realm = try Realm() 96 // MARK: - 型情報は変換しない 97// let storedRegisteredMovies = Array(realm.objects(RegisteredMovie.self)) 98// registeredMovies = storedRegisteredMovies 99 registeredMovies = realm.objects(RegisteredMovie.self) 100 101 table.reloadData() 102 } catch { 103 print(error) 104 } 105 } 106 107 private func updateRegisteredMovie(of id: Int, isWatched: Bool) { 108 109 guard let index = registeredMovies.firstIndex(where: { $0.id == id }) else { 110 return 111 } 112 do { 113 let realm = try Realm() 114 try realm.write { 115 registeredMovies[index].isWatched = isWatched 116 } 117 118 table.reloadData() 119 } catch { 120 print(error) 121 } 122 } 123 124 //データを削除するメソッド 125 func deleteRegisteredMovie(of id: Int) { 126 127 guard let index = registeredMovies.firstIndex(where: { $0.id == id }) else { 128 return 129 } 130 do { 131 let realm = try Realm() 132 try realm.write { 133 realm.delete(registeredMovies[index]) 134 } 135 136 // MARK: deleteRows() をつかうのでれば、reloadData() は呼び出さない 137 //self.table.reloadData() 138 } catch { 139 print(error) 140 } 141 } 142 143 //swipe action 144 func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 145 146 // シェアのアクションを設定する 147 let shareAction = UIContextualAction(style: .normal , title: "完了") { 148 (ctxAction, view, completionHandler) in 149 150 //データをcase1に更新 151 let selectedRegisterdMovieID = self.dataSorce[indexPath.row].id 152 let isWatched = self.segmentedControl.selectedSegmentIndex == 0 153 self.updateRegisteredMovie(of: selectedRegisterdMovieID, isWatched: isWatched) 154 155 completionHandler(true) 156 self.table.reloadData() 157 } 158 159 // シェアボタンのデザインを設定する 160 let shareImage = UIImage(systemName: "arrowshape.turn.up.right.fill")?.withTintColor(UIColor.white, renderingMode: .alwaysTemplate) 161 shareAction.image = shareImage 162 shareAction.backgroundColor = UIColor(red: 0/255, green: 125/255, blue: 255/255, alpha: 1) 163 164 // 削除のアクションを設定する 165 let deleteAction = UIContextualAction(style: .destructive, title:"削除") { 166 (ctxAction, view, completionHandler) in 167 168 //疑問点 169 let selectedDeleteMovieID = self.dataSorce[indexPath.row].id 170 self.deleteRegisteredMovie(of: selectedDeleteMovieID) 171 172 // MARK: deleteRows() をつかうのでれば、reloadData() は呼び出さない 173 tableView.deleteRows(at: [indexPath], with: .automatic) 174 175 completionHandler(true) 176 } 177 178 // スワイプでの削除を無効化して設定する 179 let swipeAction = UISwipeActionsConfiguration(actions:[deleteAction, shareAction]) 180 swipeAction.performsFirstActionWithFullSwipe = false 181 182 return swipeAction 183 } 184} 185 186extension ListViewController: UITableViewDataSource { 187 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 188 return dataSorce.count 189 } 190 191 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 192 let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 193 194 cell.textLabel?.text = dataSorce[indexPath.row].title 195 196 return cell 197 } 198}

投稿2020/09/12 23:13

TsukubaDepot

総合スコア5086

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Ytan

2020/09/14 02:32

上記の方法で削除ができました。大変助かりました。ありがとうございます。 元々Realmを使用する場合Results<>が基本形のようですね。 Results<Element>は配列なのでしょうか? firstIndex(where: { $0.id == id })が使用できるのでおそらくそうのだと思いますが、、 また deleteRows() をつかうのでれば、reloadData() は呼び出さない とありますがなぜでしょうか?
TsukubaDepot

2020/09/14 03:01

Collectionの一種ですね。 https://realm.io/docs/swift/latest/api/Classes/Results.html ただ、 Array で使えるメソッドが全て使えるわけではありませんので、上記のページなどで確認していただければとおもいます。 deleteRows() と reloadData() を併用すると、削除と読み込みのタイミングによっては、TableView に表示されているデータと Ytan さんのコードでいうところの dataSource の数などにおける整合性が取れないことがあるためです。 ここから先は推測ですが、deleteRow はアニメーションを開始した時点で呼び出しを終了するため、引き続き reloadData を読み込むとアニメーションが継続中にもかかわらず reload が行われてしまうため、整合性が取れなくなってしまうのではないでしょうか。
TsukubaDepot

2020/09/14 03:01

逆に、アニメーションが必要なければ、deleteRows は呼び出さず、reloadData を呼び出すことになります。
Ytan

2020/09/16 12:47

返信が遅れてしまいすみません。 リンクありがとうございます。参考になりました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問