回答編集履歴

1

回答追記

2022/06/15 02:38

投稿

TakeOne
TakeOne

スコア6299

test CHANGED
@@ -57,3 +57,96 @@
57
57
 
58
58
  そして、このように @ObservedResultsを使ってappend/removeを実施する処理にすれば
59
59
  Userクラスにextensionで追加した `findAll`、`add`、`delete`のstaticメソッドは不要になります。
60
+
61
+ ---
62
+ (6/15追記)
63
+
64
+ > @ObservedResultsをについてもう少しお伺いたいのですが、 ViewにViewModelからusersデータを持ってくるのと(下記のコード)、 Modelからusersデータを持ってくる(今回のコード)やり方では どちらがいいなどはあるのでしょうか? ※最終的にはどちらも同じになると思うのですが...
65
+
66
+ ``` swift
67
+ @ObservedObject var viewModel = UserViewModel() ForEach(viewModel.users.sorted { $0.postedDate > $1.postedDate }, id: \.self)
68
+ ```
69
+
70
+ このコードは、viewModelからUserデータを持ってくるのはいいのですが、 `viewModel.users` がデータベース内に格納されているデータを `Array()` で `[User]` (Userの配列)に変換して全てのUserデータをメモリ上の配列に読み込んでいるのが問題です。実際にはUserデータは1000件もないのだろうと思うので現実的に問題が発生することはないと思いますが、もし10万件存在するデータを同じ方法で処理していたら、10万件のデータを全てメモリ上の配列に読み込もうとしてメモリ不足のエラーになります。データベースから取得した型(Results<User>型や@ObservedResults)を使って処理すれば、画面に表示しているデータしかメモリに読み込まないので、100万件のデータが保存されていてもメモリ不足になることはありません。それが、データベースのメリットをあまり活かせていない問題のあるやり方であり、@ObservedResultsを使ったやり方とは大きく異るということです。
71
+
72
+
73
+ そこで、Arrayを使用せず、@ObservedResultsを使用した上で、ViewModelからUserデータを取得できるようにする方法を考えるとよさそうですが、
74
+ 普通に考えるとUserViewModelの中で
75
+ ``` swift
76
+ @ObservedResults(User.self, sortDescriptor: SortDescriptor(keyPath: "create_date", ascending: false)) var users
77
+ ```
78
+ を宣言し、UserViewの中で
79
+ ``` swift
80
+ ForEach(viewModel.users)
81
+ ```
82
+ のようにしてviewModelのusersを使用すれば、Arrayに変換しないデータをviewModelから提供できるので一番よさそうです。
83
+
84
+ しかし、実際にやってみると、この場合データの追加/削除がうまく画面に反映されませんでした。調べてみると、
85
+
86
+ https://github.com/realm/realm-swift/issues/7712
87
+
88
+ にある通り、 `@ObservedResults` は、View以外の場所で宣言すると、データの変更が検知されなくなるので、うまく動作しないようです。
89
+
90
+ そのため、 `@ObservedResults` を使ってデータの一覧を表示するには、今回の質問でやっているようにViewの中で宣言する(Realmの機能を直接使う)しかないようです。(ViewModelの中で宣言した `@ObservedResults` は、6/13の回答のようにappend/removeメソッドを使うことはできるようです。)
91
+
92
+ 表示するデータを(Arrayに変換せずに)viewModelで宣言し、Viewに表示するデータをviewModelから提供するよう統一したいなら、
93
+
94
+ https://software.small-desk.com/development/2022/01/28/swiftui-realm-create-todoapp/
95
+
96
+ で解説されているように `@ObservedResults` を使用せず、データベースから取得した一覧を@Publishedの変数で宣言して、データの追加/削除をした時にobjectWillChange.send()で通知し、UserViewでfreezeするしかないようです。
97
+
98
+ 具体的には、
99
+ UserView.swift
100
+ ``` swift
101
+ @ObservedResults(User.self, sortDescriptor: SortDescriptor(keyPath: "create_date", ascending: false)) var users
102
+
103
+ @ObservedObject var viewModel = UserViewModel()
104
+ ```
105
+ ``` swift
106
+ ForEach(users) { user in
107
+
108
+ ForEach(viewModel.users.freeze()) { user in
109
+ ```
110
+ UserViewModel.swift
111
+ ``` swift
112
+ @ObservedResults(User.self) var users
113
+
114
+ @Published var users = User.findAll().sorted(byKeyPath: "create_date")
115
+ ```
116
+ ``` swift
117
+ self.$users.append(user)
118
+
119
+ User.add(user)
120
+ self.objectWillChange.send()
121
+ ```
122
+ ``` swift
123
+ self.$users.remove(user)
124
+
125
+ User.delete(user)
126
+ self.objectWillChange.send()
127
+ ```
128
+ User.swift
129
+ ``` swift
130
+ 以下のメソッドを復活
131
+ static func findAll() -> Results<User> {
132
+ realm.objects(self)
133
+ }
134
+
135
+ static func add(_ user: User) {
136
+ try! realm.write {
137
+ realm.add(user, update: .all)
138
+ }
139
+ }
140
+
141
+ static func delete(_ user: User) {
142
+ let actualUser = realm.object(ofType: User.self, forPrimaryKey: user.id)!
143
+ try! realm.write {
144
+ realm.delete(actualUser)
145
+
146
+ }
147
+ }
148
+ ```
149
+ とすることでうまく処理できることを確認しました。
150
+
151
+ 結局、MVVMアーキテクチャを厳密に守り、Viewに提供するデータを全てViewModelが管理する構造にすることを優先するか、Realmが新しく提供した `@ObservedResults` を活かして、Viewに表示するデータを直接Modelから取得できる部分は直接使用することを許容してコードを簡潔にすることを優先するか、どちらを選択するかの問題だと思います。(自分の考え次第だと思います。)
152
+ RealmもSwiftUIに対応した機能を少しずつ拡張しているようなので、そのうちMVVMに対応した形でもっとスマートに書ける機能をRealmが提供するかもしれません。