具体的には、要素を追加したり順序を変更したりします。
Realm シリーズ第2回です。関係回は、以下です。
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その1 : 要素作成)
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その3 : UNDO/REDO 実装)
[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その4: List 表示している要素を修正する)
Sponsor Link
想定するアプリ概要
以下のような機能を追加していきます。
- 要素作成
- “要素追加”ボタンを押すと、要素が追加され、リストが更新される
- 要素削除
- “編集”ボタンを押下後、削除指定することで要素が削除され、リストが更新される
- 順序入れ替え
- “編集”ボタンを押下後、要素をドラッグすることで、順序を入れ替えることができ、リストも更新される。
モデル準備
前回、要素追加のための関数を追加していますので、削除用の関数を、AppModel に追加しました。
func removeTextLine(_ textLine: TextLine) {
guard let index = textLineList.index(of: textLine) else { return }
let realm = try! Realm()
try! realm.write {
realm.delete(textLine)
textLineList.remove(at: index)
}
}
UI 準備
以下は、いずれも、ContentView に対して変更を入れています。
編集ボタンと追加ボタン
編集ボタンは、EditButton を使うことにします。
追加ボタンは、ボタンを表示させ、押下された時に、適当な文字列を設定した TextLine を作成することとします。
複雑な機能ではないので、closure に実装し、関数化していません。(コード全体は後ほど)
List {
// snip
}
.navigationBarItems(leading: EditButton()
, trailing: Button("Add", action: {
let newName = "name" + String(Int.random(in: 0...2000))
self.appModel.addTextLine(newName)
})
)
削除と順序入れ替え
.onDelete, .onMove を使って実装していきます。
表示に使用している List の中の ForEach に以下のように追加していきます。
List {
ForEach( textLines ) { textLine in
Text("\(textLine.id) \(textLine.text)")
}
.onDelete(perform: delete)
.onMove(perform: move)
}
perform 後の delete と move は、以下で実装している ContentView の関数です。
削除実装
.onDelete から使われる機能を実装します。
お約束通り、IndexSet を受け取って処理するようにしています。
func delete(at offsets:IndexSet) {
if let realm = textLines.realm {
try! realm.write {
realm.delete(textLines[offsets.first!])
}
} else {
textLines.remove(at: offsets.first!)
}
}
注意点としては、List から削除しただけでは、Realm 中に存在していますので、Realmからもきちんと削除するようにしています。
順序入れ替え
.onMove から使われる機能を実装します。
以下が、移動関数です。こちらは引数として IndexSet と Int を受け取って処理するようになっています。
func move(fromOffsets offsets: IndexSet, toOffset to: Int) {
textLines.realm?.beginWrite()
textLines.move(fromOffsets: offsets, toOffset: to)
try! textLines.realm?.commitWrite()
}
動かない???
ここで一度動かしてみると、削除すると例外が発生してアプリがクラッシュすることがわかります。
*** Terminating app due to uncaught exception 'RLMException', reason: 'Index 12 is out of bounds (must be less than 12).'
理由は、SwiftUI は削除等の動作を行う時にアニメーションを入れてくれます。このアニメーションを行うためには、動作の前後の差異を理解する必要があります。
また、SwiftUI は、リスト等の対象は、immutable (変更されない) と想定しています。
Realm は、変更が発生すると直接そのオブジェクトを変更してしまいます。
# どちらもそれぞれの想定で実装されているのでどちらが悪いということではありません。
このため、以前に渡したリストを SwiftUI がアクセスしようとして不整合が発生しエラーになっています。
今回は削除操作だったため、リストのサイズが変わってしまっています。この点が、SwiftUI が想定していない点です。
Realm は、Realm 5.0 以降で、SwiftUI 対応されています。
具体的には、Collection タイプに対して、freeze() という関数が定義され、immutable な 集合を返してくれます。この集合を SwiftUI に渡すことで不整合を防げます。
以下が、修正も含めた ContentView.swift です。
//
// ContentView.swift
// Shared
//
// Created by Tomoaki Yagishita on 2020/09/05.
//
import SwiftUI
import RealmSwift
struct ContentView: View {
var appModel: AppModel
@ObservedObject var textLines: RealmSwift.List
var body: some View {
NavigationView {
List {
ForEach( textLines.freeze() ) { textLine in // 1
Text("\(textLine.id) \(textLine.text)")
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.navigationBarItems(leading: EditButton()
, trailing: Button("Add", action: {
let newName = "name" + String(Int.random(in: 0...2000))
self.appModel.addTextLine(newName)
})
)
.navigationBarTitle("Realm Doc")
}
HStack {
Text("# of item \(textLines.count)")
}
}
func delete(at offsets:IndexSet) {
if let realm = textLines.realm {
try! realm.write {
realm.delete(textLines[offsets.first!])
}
} else {
textLines.remove(at: offsets.first!)
}
}
func move(fromOffsets offsets: IndexSet, toOffset to: Int) {
textLines.realm?.beginWrite()
textLines.move(fromOffsets: offsets, toOffset: to)
try! textLines.realm?.commitWrite()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appModel: .init(), textLines: .init())
}
}
1: freeze() を追加しただけですが、実行してもエラーが発生しないのが確認できます。
まとめ
Realm 内部の変更に対応させるためには、immutable Collection を SwiftUI に渡すことがポイント
おすすめの Realm 本
Web で API 等を Reference で確認することができますが、じっくりと腰を据えて学習したいときには、学習の順番が考慮されている本を使うのがおすすめです。
Realm については、本がほとんど出版されていませんが、以下の本は おすすめです。
説明は以上です。
Sponsor Link