Realm から取得した複数要素も freeze() 指定して使うことで、List 等にも使えます。
ただし、この2つを組み合わせようとすると 少し工夫が必要となります。その工夫を説明します。
Realm シリーズ第4回です。以前の回は、以下です。
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その1 : 要素作成)
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その2 : 要素編集)
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その3 : UNDO/REDO 実装)
Sponsor Link
これまでの復習
ここまでみてきたところで、以下のことがわかっています。
- Realm の Object は、@ObservedObject で受けられる
- List に渡すときは、freeze() 指定しないと、エラーになる
詳細ビューで変更するパターンへの対応
リストによって要素が列挙されている画面から、1要素をクリックして詳細画面へ遷移して、要素の編集を行うパターンは、定番の操作です。
これまでの方法では、このパターンを実装しようとするとエラーが発生してしまいます。
これまでの延長線上での実装
List に渡すときには、freeze() して渡さないといけません、また、Realm の Object は、@ObservedObject で受けられるので、
普通に作ると以下のように実装出来そうです。
struct ContentView: View {
@ObservedObject var appModel: AppModel
@ObservedObject var textLines: RealmSwift.List
var body: some View {
VStack {
NavigationView {
List {
ForEach( textLines.filter("askedForDelete == false").freeze() ) { textLine in
// (1) ナビゲーション先で編集予定
NavigationLink(textLine.text, destination: TextDetailView(textLine: textLine.id)))
// Text("\(textLine.askedForDelete.description) \(textLine.id) \(textLine.text)").font(.footnote)
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.navigationBarItems(leading: HStack {
Button(action: { self.appModel.undo() }, label: {
Image(systemName: "arrow.uturn.left.circle")
})
.disabled(self.appModel.canUndo != true)
Button(action: { self.appModel.redo() }, label: {
Image(systemName: "arrow.uturn.right.circle")
})
.disabled(self.appModel.canRedo != true)
},
trailing: HStack {
EditButton()
Button("Add", action: {
let newName = "name" + String(Int.random(in: 0...2000))
let createCommand = CreateTextLineCommand(appModel: self.appModel, newTextLineText: newName)
self.appModel.executeCommand(createCommand)
})
}
)
.navigationBarTitle("Realm Doc")
}
HStack {
Text("# of item \(textLines.filter("askedForDelete == false").count)")
}
}
}
/* snip */
}
// (2)
struct TextDetailView: View {
// (3)
@ObservedObject var textLine: TextLine
var body: some View {
TextField(textLine.text, text: $textLine.text)
}
}
(1) で、下位ビューに移行するようにして、(2) で定義している下位ビューで、テキストを編集できるようにしています。
変更を伝播するため、(3) で @ObservedObject で受けています。
実行すると・・・
以下のようなエラーが発生します。
Exception NSException * "Frozen Realms do not change and do not have change notifications."
理由は、以下の箇所で (a) で freeze() して渡した要素を、(b) で Binding して渡そうとしている点です。
// ⬇️ (a)
ForEach( textLines.filter("askedForDelete == false").freeze() ) { textLine in
// ⬇️ (b)
NavigationLink(textLine.text, destination: textLine)
}
Realm のドキュメントには、以下のように説明されています。
- Opening a write transaction on a frozen realm.
- Modifying a frozen object.
- Adding a change listener to a frozen realm, collection, or object.
解決策も以下のように書かれています。
そのままですね。再度、Query しなさいとのことでした。
再 Query するように修正
準備として id から要素を取得する以下のような関数を作ります。
func textLineFromID(id: String) -> TextLine {
let realm = try! Realm()
return realm.object(ofType: TextLine.self, forPrimaryKey: id)!
}
そして、TextDetailView へ渡す TextLine を、再取得してから渡すようにします。
ForEach( textLines.filter("askedForDelete == false").freeze() ) { textLine in
NavigationLink(textLine.text, destination: TextDetailView(textLine: self.textLineFromID(id: textLine.id)))
}
改めて実行してみると
別のエラーが・・・・
Exception NSException * "Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first."
TextField に String の Binding を渡して変更しようとしているのですが、Realm では、Object の変更は、以下のように、write ブロックで変更しなければいけません。
try! realm.write {
// Realm 要素の変更処理等
}
TextField には、onCommit: で編集終了時の処理を記述する方法がありますので、以下のようにします。
TextField(textLine.text, text: $tmpText, onCommit: {
let realm = try! Realm()
try! realm.write {
textLine.text = tmpText
}
})
テキストを変更するコマンドを作って、onCommit で実行するほうが良いですが、省略しています。
やっと・・・
SwiftUI の期待するやり方と、Realm の期待するやり方をうまく整合させることが必要になりましたが、スムーズに動作させることができました。
参考までに、改めて、コードを提示します。
struct ContentView: View {
@ObservedObject var appModel: AppModel
@ObservedObject var textLines: RealmSwift.List
var body: some View {
VStack {
NavigationView {
List {
ForEach( textLines.filter("askedForDelete == false").freeze() ) { textLine in
NavigationLink(textLine.text, destination: TextDetailView(textLine: self.textLineFromID(id: textLine.id)))
}
.onDelete(perform: delete)
}
.navigationBarItems(leading: HStack {
Button(action: { self.appModel.undo() }, label: {
Image(systemName: "arrow.uturn.left.circle")
})
.disabled(self.appModel.canUndo != true)
Button(action: { self.appModel.redo() }, label: {
Image(systemName: "arrow.uturn.right.circle")
})
.disabled(self.appModel.canRedo != true)
},
trailing: HStack {
EditButton()
Button("Add", action: {
let newName = "name" + String(Int.random(in: 0...2000))
let createCommand = CreateTextLineCommand(appModel: self.appModel, newTextLineText: newName)
self.appModel.executeCommand(createCommand)
})
}
)
.navigationBarTitle("Realm Doc")
}
HStack {
Text("# of item \(textLines.filter("askedForDelete == false").count)")
}
}
}
func delete(at offsets:IndexSet) {
let target = textLines.filter("askedForDelete == false")[offsets.first!]
let deleteCommand = DeleteTextLineCommand(appModel: self.appModel, targetId: target.id, newValue: true)
self.appModel.executeCommand(deleteCommand)
}
}
func textLineFromID(id: String) -> TextLine {
let realm = try! Realm()
return realm.object(ofType: TextLine.self, forPrimaryKey: id)!
}
}
struct TextDetailView: View {
@ObservedObject var textLine: TextLine
@State private var tmpText = ""
var body: some View {
TextField(textLine.text, text: $tmpText, onCommit: {
let realm = try! Realm()
try! realm.write {
textLine.text = tmpText
}
})
}
}
まとめ
- Realm の Object は、Observable なので、そのまま使える
- SwiftUI の List に渡すには、freeze() して渡す
- freeze した Realm Object は、再度 Query してから、変更する
- Realm Object は、Realm.write ブロックで変更する必要があるので、onCommit 等を使用して、自分で変更処理を行う
SwiftUI 学習におすすめの本
SwiftUI 徹底入門
SwiftUI は、グラフィカルなライブラリということもあり、文字だけのテキストよりは、画像が多く入れられた書籍を読むと理解が進みやすいです。
自分で購入した中でおすすめできるものとしては、以下のものです。
2019 年発表の SwiftUI 1.0 相当を対象にしているので、2020/2021 に追加された一部の機能は、説明されていません。
ですが、SwiftUI 入門書としては、非常によくできていますし、わかりやすいです。 この本で学習した後に、追加分を学習しても良いと思います。
SwiftUIViewsMastery
英語での説明になってしまいますが、以下の本もおすすめです。
1ページに、コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。
Sponsor Link