[Swift/SwiftUI]Realmでデータベースの実装(データのソート)

目次

概要

データベースのデータを表示するのはいいが、数が多くなってくるとどこにどのデータがあるかわかりづらいし、何よりもユーザーにも不親切。
その問題を解消するためにソートを行う。
本コードは他のページのRelmのページのコードに追記している。

ソースコード

import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            DatabaseListView()
        }
    }
}

View関連

simport SwiftUI

struct DatabaseListView: View {
    @ObservedObject var viewModel: DatabaseListViewModel = DatabaseListViewModel()
    
    @State var inputText: String = ""
    
    var body: some View {
        VStack {
            Spacer()
            HStack {
                Spacer()
                TextField("", text: self.$inputText)
                    .textFieldStyle(.roundedBorder)
                Spacer()
                Button {
                    self.viewModel.registerData(inputText: self.inputText)
                    self.inputText = ""
                } label: {
                    Text("登録")
                }
                Spacer()
            }
            Spacer()
            List {
                ForEach(self.viewModel.data, id: \.self) { data in
                    VStack(alignment: .leading) {
                        Text("ID: \(data.id)")
                        Text("入力文字: \(data.title)")
                        Text("登録日: \(data.registerDate.transeJapaneseDateString())")
                        Text("更新日: \(data.updateDate.transeJapaneseDateString())")
                    }
                }
            }
        }
        .alert(isPresented: self.$viewModel.isShownDialog) {
            Alert(title: Text("Error"),
                  message: Text("登録に失敗しました"),
                  dismissButton: .default(Text("閉じる")))
        }
    }
}

処理部分

import Foundation
import RealmSwift

class DatabaseListViewModel: ObservableObject {
    @Published var isShownDialog: Bool = false
    @Published var data: [DatabaseTableData]
    
    init() {
        self.data = DatabaseManager.shared.getInstance()
    }
    
    func registerData(inputText: String) {
        let data = DatabaseTableData(id: self.data.count + 1,
                                     title: inputText,
                                     registerDate: Date(),
                                     updateDate: Date())
        DatabaseManager.shared.registerFavoriteSetting(data: data)
        self.data = DatabaseManager.shared.getInstance()
    }
}

日付表示部分のExtension

import Foundation

extension Date {
    func transeJapaneseDateString() -> String {
        let format = Date.FormatStyle().locale(Locale(identifier: "ja_JP"))
                    .year()
                    .month(.twoDigits)
                    .day(.twoDigits)
                    .weekday(.abbreviated)
                    .hour(.twoDigits(amPM: .wide))
                    .minute(.twoDigits)
                    .second(.twoDigits)
        return format.format(self)
    }
}

DB処理部分

データベースの項目クラス(テーブルのカラム設定のようなもの)

import Foundation
import RealmSwift

class DatabaseTableData: Object {
    @objc dynamic var id: Int
    @objc dynamic var title: String
    @objc dynamic var registerDate: Date
    @objc dynamic var updateDate: Date
    
    override init() {
        self.id = 0
        self.title = ""
        self.registerDate = Date()
        self.updateDate = Date()
    }
    
    init(id: Int, title: String, registerDate: Date, updateDate: Date) {
        self.id = id
        self.title = title
        self.registerDate = registerDate
        self.updateDate = updateDate
    }
}

DBの処理部分

import Foundation
import RealmSwift

enum DataBaseOrder: String, CaseIterable, Identifiable {
    case idAscendingOrder = "ID昇順"
    case idDescendingOrder = "ID降順"
    case titleAscendingOrder = "タイトル昇順"
    case titleDescendingOrder = "タイトル降順"
    case registerDateAscendingOrder = "登録が古い順"
    case registerDateDescendingOrder = "登録が新しい順"
    case updateDateAscendingOrder = "更新が古い順"
    case updateDateDescendingOrder = "更新が新しい順"
    
    var id: String { rawValue }
}


class DatabaseManager {
    static let shared = DatabaseManager()
    var realm: Realm? = nil
    
    @Published var selectedOrder: DataBaseOrder = .idAscendingOrder
    
    private init() {
        
    }
    
    func getInstance() -> [DatabaseTableData] {
        do {
            self.realm = try? Realm()
        }
        guard var objects = self.realm?.objects(DatabaseTableData.self) else {
            return []
        }
        objects = sortedObjects(objects: objects)
        return Array(objects)
    }
    
    func registerData(data: DatabaseTableData) {
        do {
            self.realm = try? Realm()
            try? self.realm?.write {
                self.realm?.add(data)
            }
        }
    }
    
    func updateData(data: DatabaseTableData, title: String) {
        do {
            self.realm = try? Realm()
            try? self.realm?.write {
                data.title = title
                data.updateDate = Date()
            }
        }
    }
    
    func deleteData(data: DatabaseTableData) {
        do {
            self.realm = try? Realm()
            try? self.realm?.write {
                self.realm?.delete(data)
            }
        }
    }
    
    private func sortedObjects(objects: Results<DatabaseTableData>) -> Results<DatabaseTableData> {
        switch DatabaseManager.shared.selectedOrder {
        case .idAscendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "id", ascending: true),
            ]
            return objects.sorted(by: sortDescriptors)
        case .idDescendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "id", ascending: false),
            ]
            return objects.sorted(by: sortDescriptors)
        case .titleAscendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "title", ascending: true),
            ]
            return objects.sorted(by: sortDescriptors)
        case .titleDescendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "title", ascending: false),
            ]
            return objects.sorted(by: sortDescriptors)
        case .registerDateAscendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "registerDate", ascending: true),
            ]
            return objects.sorted(by: sortDescriptors)
        case .registerDateDescendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "registerDate", ascending: false),
            ]
            return objects.sorted(by: sortDescriptors)
        case .updateDateAscendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "updateDate", ascending: true),
            ]
            return objects.sorted(by: sortDescriptors)
        case .updateDateDescendingOrder:
            let sortDescriptors = [
                SortDescriptor(keyPath: "updateDate", ascending: false),
            ]
            return objects.sorted(by: sortDescriptors)
        }
    }
}

デモ動画

詳細

ソートする際に必要なのはソートする種類を格納する列挙体。
ソースコードの例では以下。

enum DataBaseOrder: String, CaseIterable, Identifiable {
    case idAscendingOrder = "ID昇順"
    case idDescendingOrder = "ID降順"
    case titleAscendingOrder = "タイトル昇順"
    case titleDescendingOrder = "タイトル降順"
    case registerDateAscendingOrder = "登録が古い順"
    case registerDateDescendingOrder = "登録が新しい順"
    case updateDateAscendingOrder = "更新が古い順"
    case updateDateDescendingOrder = "更新が新しい順"
    
    var id: String { rawValue }
}

文字列はViewに記載したソート用のボタンに表示する文言として定義している。
そのため、単に処理だけを行う場合は文字列は不要。今回は画面右上にソートボタンを設置したかったため、文字列まで定義している。
ちなみに、ソートボタンの実装部分は以下。

.navigationTitle("データベーステスト")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
        listOrderMenu()
    }
}
private func listOrderMenu() -> some View {
    return Menu {
        ForEach(DataBaseOrder.allCases, id:\.self) { dataOrder in
            Button {
                DatabaseManager.shared.selectedOrder = dataOrder
                viewModel.data = DatabaseManager.shared.getInstance()
            } label: {
                HStack {
                    if dataOrder == DatabaseManager.shared.selectedOrder {
                        Image(systemName: "checkmark")
                    }
                    Text(dataOrder.rawValue)
                }
            }
        }
    } label: {
        Image(systemName: "arrow.up.and.down.text.horizontal")
    }
}

そして、実際のDBのソート処理は以下

func getInstance() -> [DatabaseTableData] {
    do {
        self.realm = try? Realm()
    }
    guard var objects = self.realm?.objects(DatabaseTableData.self) else {
        return []
    }
    objects = sortedObjects(objects: objects)
    return Array(objects)
}
private func sortedObjects(objects: Results<DatabaseTableData>) -> Results<DatabaseTableData> {
    switch DatabaseManager.shared.selectedOrder {
    case .idAscendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "id", ascending: true),
        ]
        return objects.sorted(by: sortDescriptors)
    case .idDescendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "id", ascending: false),
        ]
        return objects.sorted(by: sortDescriptors)
    case .titleAscendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "title", ascending: true),
        ]
        return objects.sorted(by: sortDescriptors)
    case .titleDescendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "title", ascending: false),
        ]
        return objects.sorted(by: sortDescriptors)
    case .registerDateAscendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "registerDate", ascending: true),
        ]
        return objects.sorted(by: sortDescriptors)
    case .registerDateDescendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "registerDate", ascending: false),
        ]
        return objects.sorted(by: sortDescriptors)
    case .updateDateAscendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "updateDate", ascending: true),
        ]
        return objects.sorted(by: sortDescriptors)
    case .updateDateDescendingOrder:
        let sortDescriptors = [
            SortDescriptor(keyPath: "updateDate", ascending: false),
        ]
        return objects.sorted(by: sortDescriptors)
    }
}

要は、何でソートするかはSortDescriptorのkeyPathにデータベースのカラムのクラスの変数名を文字列として設定し、
昇順にするか降順にするかはascendingをtrueにしたら昇順、falseにしたら降順になる。
仕上げに、データを取得したobjectsのsortedメソッドを使用してデータのソートの処理を行う。

参考ページ:
徒然帳「RealmSwift 複数条件のソート(並べ替え)」