##はじめに
SwiftUIのListの並び替え(.onMove)を用いるアプリで、Realmと連携しデータ保存を行う方法をまとめました。
↓こんなサンプルアプリを作成しました。
##環境
Swift 5.3.1
Xcode 12.2
Cocoapods 1.10.0
RealmSwift 10.1.4
##経緯
SwiftUIでRealmを使用し始め、Listの並び替えの .onMove が中々上手く動作させられなかったので、同じような悩みの人もいるのでは?と思い書きました。
SwiftUIのListの並び替えをRealmに対応させる情報は見つけられなかった(無くは無いが、私には理解できなかった)ので、私でもできる方法が無いか模索しました。
その結果、UITableViewとRealmの例を参考にすることで、実装することができました。
##実装した動作・機能
- リストの追加
- テキストフィールドに文字を入力し、決定ボタンでその文字をリストに追加。
- EditButton
- EditModeのオンオフができるボタン。
- リストの並び替えや削除を行うために、SwiftUIにあらかじめ準備されているもの。
- リストの並び替え
- EditModeがオンの時、Listの並び替えを可能に。
- リストの削除
- リストの左スワイプ、及びEditModeがオンの時、削除用のボタンタップで削除。
- Realmで保存
- DBにデータを保存。アプリを落としてもデータが保存されている。
##ソースコード
####RealmDBのデータを定義
- id : DBの主キー用のID
- title : リストに表示するタイトル
- order : リストを並び替える際に使用する数値
ポイントはorderを入れることです。このorderを変化させることで、並び替えを実現できました。
import RealmSwift
class ItemDB: Object {
@objc dynamic var id = ""
@objc dynamic var title = ""
@objc dynamic var order = 0
override class func primaryKey() -> String? {
"id"
}
}
####RealmDBと同じ要素を持つ構造体Itemを作成
import Foundation
struct Item: Identifiable {
let id: String
let title: String
let order: Int
}
extension Item {
init(itemDB: ItemDB) {
id = itemDB.id
title = itemDB.title
order = itemDB.order
}
}
####ItemStoreの作成
ここのitemsを用いて、DBのデータを取ってきています。
.sorted(byKeyPath: "order")
で、orderの数値を用いて、itemResultsを並び替えています。
import RealmSwift
final class ItemStore: ObservableObject {
private var itemResults: Results<ItemDB>
init(realm: Realm) {
itemResults = realm.objects(ItemDB.self)
.sorted(byKeyPath: "order") // orderの数値で並び替え
}
var items: [Item] {
itemResults.map(Item.init)
}
}
ItemStoreには、Realmを操作する関数も記述しています。
extension ItemStore {
// データの追加
func create(title: String, order: Int) {
objectWillChange.send()
do {
let realm = try Realm()
let itemDB = ItemDB()
itemDB.id = UUID().uuidString
itemDB.title = title
itemDB.order = order
try realm.write {
realm.add(itemDB)
}
} catch let error {
print(error.localizedDescription)
}
}
// データの削除
func delete(id: String) {
objectWillChange.send()
guard let itemDB = itemResults.first(where: { $0.id == id }) else {
return
}
do {
let realm = try Realm()
try realm.write {
realm.delete(itemDB)
}
} catch let error {
print(error.localizedDescription)
}
}
// データの更新
func update(id: String, order: Int) {
objectWillChange.send()
do {
let realm = try Realm()
try realm.write {
realm.create(ItemDB.self,
value: ["id": id,
"order": order],
update: .modified)
}
} catch let error {
print(error.localizedDescription)
}
}
// Listを並び替えるための関数
func move(sourceIndexSet: IndexSet, destination: Int) {
guard let source = sourceIndexSet.first else {
return
}
// 並び替える行のIDを取得
let moveId = items[source].id
// source、destinationの値については、参考資料を参考にしてください。
// Listの行を下に移動する場合
if source < destination {
for i in (source + 1)...(destination - 1) {
update(id: items[i].id, order: items[i].order - 1)
}
update(id: moveId, order: destination - 1)
// Listの行を上に移動する場合
} else if destination < source {
// reversed()で逆から回さないと、一時的にorderの数値が重なり、想定外の挙動を示します。
for i in (destination...(source - 1)).reversed() {
update(id: items[i].id, order: items[i].order + 1)
}
update(id: moveId, order: destination)
} else {
return
}
}
}
####Listを表示するItemListViewの作成
import SwiftUI
struct ItemListView: View {
@EnvironmentObject var store: ItemStore
@State var title = ""
var items: [Item]
var body: some View {
VStack {
EditButton()
.padding(.top)
HStack {
TextField("タイトルの入力", text: $title)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("決定", action: create)
}
.padding(.leading)
.padding(.trailing)
List {
ForEach(items) { item in
HStack {
Text(item.title)
Spacer()
// orderの番号が分かりやすいように表示
Text("order:\(item.order)")
}
}
.onDelete { offsets in
delete(offsets: offsets)
}
.onMove { source, destination in
move(sourceIndexSet: source, destination: destination)
}
}
}
}
}
ItemListViewに関数(create, delete, move)を記述。
extension ItemListView {
private func create() {
store.create(title: self.title, order: items.count)
self.title = ""
}
private func delete(offsets: IndexSet) {
guard let index = offsets.first else {
return
}
// 削除する行のIDを取得
let deleteId = items[index].id
// 削除する行の行番号を取得
let deleteOrder = items[index].order
// 削除する行の行番号より大きい行番号を全て -1 する
for i in (deleteOrder + 1)..<items.count {
store.update(id: items[i].id, order: items[i].order - 1)
}
// 行を削除する
store.delete(id: deleteId)
}
private func move(sourceIndexSet: IndexSet, destination: Int) {
store.move(sourceIndexSet: sourceIndexSet, destination: destination)
}
}
####ContentsViewとSceneDelegateの書き換え
struct ContentView: View {
@EnvironmentObject var store: ItemStore
var body: some View {
ItemListView(items: store.items)
}
}
// 一部抜粋
// import RealmSwiftしてください。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
do {
let realm = try Realm()
let window = UIWindow(windowScene: windowScene)
let contentView = ContentView()
.environmentObject(ItemStore(realm: realm))
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
} catch let error {
fatalError("Failed to open Realm.Error:\(error.localizedDescription)")
}
}
}
これで一通り完了です。
EditModeにして並び替えてみると、orderの数値の変化に伴って並び変わります。
以上となります!
##参考資料
この記事でorderを追加する着想を得ました。こちらUITableViewでの例です。
realmをデータソースにしてテーブルビューの並べ替えをしたい(stackoverflow)
この記事でonMoveのsourceとdestinationの示す値について勉強しました。
【SwiftUI】onMove時の destination の値が並び替え方向の違いで異なる謎。
SwiftUIでRealmを使う分かりやすいサンプルを作れる記事です。
Listの並び替えとは関係ないですが、コードの書き方全般はこの記事に則っています。
Realm with SwiftUI Tutorial: Getting Started(raywenderlich.com)
##おわりに
最後まで読んでくださりありがとうございます。
私はListの並び替えで結構悩んだので、皆さんのその時間をすっ飛ばせるような記事になっていれば幸いです。
参考資料のUITableViewの例で、RealmのListを用いる手法も挙げられていました。
しかし、私自身のRealmの知識が乏しく、理解できておりません。
その手法を用いた方が、より簡単に並び替えを実装できるかもしれないので、その検討も随時進めていきます。
逆に、これ良いよ!的なのがありましたら教えてくださると嬉しいです!
最後に、SwiftUIとRealmで追加・削除・更新を行うサンプルアプリの記事も書いています。
SwiftUIでRealmを使う導入的な内容です。良かったら見てみてください。
SwiftUIでRealmを使ってみた