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

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

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

RealmとはSQLiteやCore Dataに代わるモバイルデータベースです。iOSとAndroidの両方でサポートされています。

Swift

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

Q&A

解決済

1回答

2150閲覧

Swiftui ObservedResultsについて

asedsa

総合スコア3

Realm

RealmとはSQLiteやCore Dataに代わるモバイルデータベースです。iOSとAndroidの両方でサポートされています。

Swift

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

0グッド

0クリップ

投稿2022/06/13 13:43

先日ここで質問させていただいたのですが、
ObservedResultsについてお伺いしたです。
また、
ObservedResultsを使いつつ、ボタンを押し削除する方法を教えていただきたいです。

・質問
具体的にはObservedResultsを使用する際に、
freezeを使用しなくてもデータを削除できると考えているのですが、
どのような書き方になるかを教えていただきたいです。

ObservedResultsを使いViewにデータ(User)を表示させているのですが、
追加することはできても削除することはできません。

コメントアウトしていますが、freezeを使えば削除可能です。(Realmからも)

また、
freezeを使わなくても下記をViewに記載すると削除可能です。(Realmからも)
※望むやり方とは違いますが...

.onDelete(perform: $users.remove)

・やりたいこと
削除を押した時、注意文言が出てきてYesの場合は削除という動作を行いたいです。
※ObservedResultsを使用しつつ、freezeを使わない方法です。

・下記コード
User.swift

import Foundation import RealmSwift class User: Object, ObjectKeyIdentifiable { @Persisted(primaryKey: true) var id: Int = 0 @Persisted var userName: String = "" @Persisted var create_date = Date() override static func primaryKey() -> String? { return "id" } } extension User { private static var config = Realm.Configuration(schemaVersion: 1) private static var realm = try! Realm(configuration: config) static func findAll() -> Results<User> { // realm.objects(self).freeze() realm.objects(self) } static func add(_ user: User) { try! realm.write { realm.add(user, update: .all) } } static func delete(_ user: User) { let actualUser = realm.object(ofType: User.self, forPrimaryKey: user.id)! try! realm.write { realm.delete(actualUser) } } }

UserViewModel.swift

import Foundation import Combine import RealmSwift class UserViewModel: ObservableObject { @Published private(set) var users: [User] = Array(User.findAll()) @Published var userTextField = "" @Published var deleteUser: User? @Published var isDeleteAllTapped = false private var addUserTask: AnyCancellable? private var deleteUserTask: AnyCancellable? init() { addUserTask = self.$userTextField .sink() { username in guard !username.isEmpty else { return } let user = User() user.id = (try! Realm().objects(User.self).max(ofProperty: "id") ?? 0) + 1 user.userName = username User.add(user) // self.users.append(user.freeze()) self.users.append(user) } deleteUserTask = self.$deleteUser .sink() { user in guard let user = user else { return } if let index = self.users.firstIndex(of: user) { do{ self.users.remove(at: index) User.delete(user) }catch { print("Error \(error)") } } } } }

UserView.swift

import SwiftUI import RealmSwift struct UserView: View { @ObservedObject var viewModel = UserViewModel() @ObservedResults(User.self, sortDescriptor: SortDescriptor(keyPath: "create_date", ascending: false)) var users @State private var isUserTextFieldPresented = false @State private var isDeleteAlertPresented = false @State private var isDeleteAllAlertPresented = false @State private var userTextField = "" @State private var deleteUser: User? var body: some View { NavigationView { VStack { if (isUserTextFieldPresented) { TextField("メモを入力してください", text: $userTextField) .textFieldStyle(DefaultTextFieldStyle()) .keyboardType(.asciiCapable) } List { ForEach(users) { user in HStack { UserRowView(user: user) Spacer() Text("削除").onTapGesture { self.deleteUser = user isDeleteAlertPresented.toggle() } .padding() .foregroundColor(.white) .background(Color.red) } .alert(isPresented: $isDeleteAlertPresented) { Alert(title: Text("警告"), message: Text("メモを削除します。\nよろしいですか?"), primaryButton: .cancel(Text("いいえ")), secondaryButton: .destructive(Text("はい")) { viewModel.deleteUser = self.deleteUser } ) } } } } .navigationTitle("メモの一覧") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("追加") { if (isUserTextFieldPresented) { viewModel.userTextField = userTextField userTextField = "" } isUserTextFieldPresented.toggle() }.disabled(isUserTextFieldPresented && userTextField.isEmpty) } } } } } // MARK: UserRowView struct UserRowView: View { var user: User var body: some View { VStack(alignment: .leading) { Text(formatDate(user.create_date)) .font(.caption) .fontWeight(.bold) Text(user.userName) .font(.body) } } func formatDate(_ date : Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .medium formatter.locale = Locale(identifier: "ja_JP") return formatter.string(from: date) } }

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

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

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

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

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

guest

回答1

0

ベストアンサー

前回の質問の回答で @ObservedResults を使うと良いというアドバイスをしましたが、freeze() を使うこと自体は問題ではありません。

@ObservedResults を使えば、自動的にfreezeされたオブジェクトの一覧が得られ、さらにそれに対して append メソッド、 remove メソッドといったデータベース更新メソッドが用意されているので、今よりもう少し簡潔に書けるというだけです。

前回の質問のコードの一番大きな問題は、前回コメントした通り

今の方法は、Realmデータベースに保存されているデータを一旦配列に全て読み込んで処理しており、データベースのメリットをあまり活かせていません。

という点です。

もっと具体的に言うと、UserViewModelの

swift

1@Published private(set) var users: [User] = Array(User.findAll())

でデータベースから読み込んだ Results<User> 型の値を Array()[Users] (Userの配列)に変換して全ての値をメモリ上の配列に読み込んでいる点です。

上記の users を定義するコードを @ObservedResults を使って

swift

1@ObservedResults(User.self) var users

とすれば、
Userを追加する処理は

swift

1 User.add(user) 2// self.users.append(user.freeze()) 3 self.users.append(user) 4

ではなく

swift

1 self.$users.append(user)

と1行で処理することができ、

Userを削除する処理は、

swift

1 if let index = self.users.firstIndex(of: user) { 2 do{ 3 4 self.users.remove(at: index) 5 6 User.delete(user) 7 }catch { 8 print("Error \(error)") 9 } 10 } 11

ではなく

swift

1 self.$users.remove(user) 2

と1行で処理することができます。

このようにするだけで、UserViewに表示しているUserの一覧もデータベースに格納しているUserの一覧も同時に更新でき、全てうまく動作するはずです。

そして、このように @ObservedResultsを使ってappend/removeを実施する処理にすれば
Userクラスにextensionで追加した findAlladddeleteのstaticメソッドは不要になります。


(6/15追記)

@ObservedResultsをについてもう少しお伺いたいのですが、 ViewにViewModelからusersデータを持ってくるのと(下記のコード)、 Modelからusersデータを持ってくる(今回のコード)やり方では どちらがいいなどはあるのでしょうか? ※最終的にはどちらも同じになると思うのですが...

swift

1@ObservedObject var viewModel = UserViewModel() ForEach(viewModel.users.sorted { $0.postedDate > $1.postedDate }, id: \.self)

このコードは、viewModelからUserデータを持ってくるのはいいのですが、 viewModel.users がデータベース内に格納されているデータを Array()[User] (Userの配列)に変換して全てのUserデータをメモリ上の配列に読み込んでいるのが問題です。実際にはUserデータは1000件もないのだろうと思うので現実的に問題が発生することはないと思いますが、もし10万件存在するデータを同じ方法で処理していたら、10万件のデータを全てメモリ上の配列に読み込もうとしてメモリ不足のエラーになります。データベースから取得した型(Results<User>型や@ObservedResults)を使って処理すれば、画面に表示しているデータしかメモリに読み込まないので、100万件のデータが保存されていてもメモリ不足になることはありません。それが、データベースのメリットをあまり活かせていない問題のあるやり方であり、@ObservedResultsを使ったやり方とは大きく異るということです。

そこで、Arrayを使用せず、@ObservedResultsを使用した上で、ViewModelからUserデータを取得できるようにする方法を考えるとよさそうですが、
普通に考えるとUserViewModelの中で

swift

1@ObservedResults(User.self, sortDescriptor: SortDescriptor(keyPath: "create_date", ascending: false)) var users

を宣言し、UserViewの中で

swift

1ForEach(viewModel.users)

のようにしてviewModelのusersを使用すれば、Arrayに変換しないデータをviewModelから提供できるので一番よさそうです。

しかし、実際にやってみると、この場合データの追加/削除がうまく画面に反映されませんでした。調べてみると、

https://github.com/realm/realm-swift/issues/7712

にある通り、 @ObservedResults は、View以外の場所で宣言すると、データの変更が検知されなくなるので、うまく動作しないようです。

そのため、 @ObservedResults を使ってデータの一覧を表示するには、今回の質問でやっているようにViewの中で宣言する(Realmの機能を直接使う)しかないようです。(ViewModelの中で宣言した @ObservedResults は、6/13の回答のようにappend/removeメソッドを使うことはできるようです。)

表示するデータを(Arrayに変換せずに)viewModelで宣言し、Viewに表示するデータをviewModelから提供するよう統一したいなら、

https://software.small-desk.com/development/2022/01/28/swiftui-realm-create-todoapp/

で解説されているように @ObservedResults を使用せず、データベースから取得した一覧を@Publishedの変数で宣言して、データの追加/削除をした時にobjectWillChange.send()で通知し、UserViewでfreezeするしかないようです。

具体的には、
UserView.swift

swift

1@ObservedResults(User.self, sortDescriptor: SortDescriptor(keyPath: "create_date", ascending: false)) var users 23@ObservedObject var viewModel = UserViewModel()

swift

1ForEach(users) { user in 23ForEach(viewModel.users.freeze()) { user in

UserViewModel.swift

swift

1@ObservedResults(User.self) var users 23@Published var users = User.findAll().sorted(byKeyPath: "create_date")

swift

1self.$users.append(user) 23User.add(user) 4self.objectWillChange.send()

swift

1self.$users.remove(user) 23User.delete(user) 4self.objectWillChange.send()

User.swift

swift

1以下のメソッドを復活 2 static func findAll() -> Results<User> { 3 realm.objects(self) 4 } 5 6 static func add(_ user: User) { 7 try! realm.write { 8 realm.add(user, update: .all) 9 } 10 } 11 12 static func delete(_ user: User) { 13 let actualUser = realm.object(ofType: User.self, forPrimaryKey: user.id)! 14 try! realm.write { 15 realm.delete(actualUser) 16 17 } 18 }

とすることでうまく処理できることを確認しました。

結局、MVVMアーキテクチャを厳密に守り、Viewに提供するデータを全てViewModelが管理する構造にすることを優先するか、Realmが新しく提供した @ObservedResults を活かして、Viewに表示するデータを直接Modelから取得できる部分は直接使用することを許容してコードを簡潔にすることを優先するか、どちらを選択するかの問題だと思います。(自分の考え次第だと思います。)
RealmもSwiftUIに対応した機能を少しずつ拡張しているようなので、そのうちMVVMに対応した形でもっとスマートに書ける機能をRealmが提供するかもしれません。

投稿2022/06/14 01:47

編集2022/06/15 02:38
TakeOne

総合スコア6299

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

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

asedsa

2022/06/14 14:51

丁寧な解説ありがとうございます。 認識違いをしていたようです。 @ObservedResultsをについてもう少しお伺いたいのですが、 ViewにViewModelからusersデータを持ってくるのと(下記のコード)、 Modelからusersデータを持ってくる(今回のコード)やり方では どちらがいいなどはあるのでしょうか? ※最終的にはどちらも同じになると思うのですが... @ObservedObject var viewModel = UserViewModel() ForEach(viewModel.users.sorted { $0.postedDate > $1.postedDate }, id: \.self)
TakeOne

2022/06/15 02:53

追加の質問を受けて回答を追加しておきました。
asedsa

2022/06/15 12:24

ありがとうございます!!!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問