しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

Realm Swift + SwiftUIでテーブル表示・編集・削除

Realm SwiftとSwiftUIを使ったテーブル実装を試してみました。

注) 2020/04/26時点でRealmはSwiftUIを公式サポートしていません。
対応状況は下Issueを参考にしてみて下さい。

github.com

表示

プロジェクトを作ったらCocoaPodsでRealmSwiftを追加します。

platform :ios, '13.0'

target 'MyApp' do
  use_frameworks!

  pod 'RealmSwift'
end

Realmの追加が終わったらEntityを作成します。
定義は下の通りです。

import UIKit
import RealmSwift

class MyModel: Object {
    @objc dynamic var id = UUID().uuidString
    @objc dynamic var title = ""
}

次はこのデータをテーブル表示します。
後々のことを考えて、データ一覧はViewModelに入れています。

import SwiftUI
import RealmSwift

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        List {
            ForEach(model.myModels, id: \.id) { myModel in
                Button(action: {
                }) {
                    Text("\(myModel.title)")
                }
            }
        }
    }
}

class ContentViewModel: ObservableObject {
    @Published var myModels: [MyModel] = (try? Realm().objects(MyModel.self).map { $0 }) ?? []
}

テストデータを3件作ってから実行すると下のようになります。
日付をそのままtitleに入れたので下のような表示になっています。

f:id:llcc:20200426201026p:plain

編集

次はデータの更新をします。
Cellをタップしたら現在時刻をtitleにセットするような実装にしたいと思います。

まずはViewModeを以下のように修正します。
ここではRealmのobserveを使って更新を受け取るように変更しています。

class ContentViewModel: ObservableObject {
    private var token: NotificationToken?
    private var myModelResults = try? Realm().objects(MyModel.self)
    @Published var myModels: [MyModel] = []
    
    init() {
        token = myModelResults?.observe { [weak self] _ in
            self?.myModels = self?.myModelResults?.map { $0 } ?? []
        }
    }
    
    deinit {
        token?.invalidate()
    }
}

続けてView側も修正します。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        List {
            ForEach(model.myModels, id: \.id) { myModel in
                Button(action: {
                    try? Realm().write {
                        myModel.title = "\(Date())"
                    }
                }) {
                    Text("\(myModel.title)")
                }
            }
        }
    }
}

これで、ボタンをタップする度にtitleが更新されるようになりました。

追加

次はデータの追加を実装します。
ナビゲーションバーの追加ボタンを押したらデータが増えるような仕様にしたいと思います。

Viewを下のように変更します。
追加したのはnavigationBarItems以下の処理です。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.myModels, id: \.id) { myModel in
                    Button(action: {
                        try? Realm().write {
                            myModel.title = "\(Date())"
                        }
                    }) {
                        Text("\(myModel.title)")
                    }
                }
            }.navigationBarItems(trailing: Button(action: {
                let myModel = MyModel()
                myModel.title = "\(Date())"
                let realm = try? Realm()
                try? realm?.write {
                    realm?.add(myModel)
                }
            }) {
                Text("Add")
            })
        }
    }
}

これでデータの追加ができるようになりました。

f:id:llcc:20200426203109p:plain

削除

最後に削除処理を実装します。
削除処理は下のようにonDeleteを付ける事で実現できます。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.myModels, id: \.id) { myModel in
                    Button(action: {
                        try? Realm().write {
                            myModel.title = "\(Date())"
                        }
                    }) {
                        Text("\(myModel.title)")
                    }
                }.onDelete { indexSet in
                    if let index = indexSet.first {
                        let realm = try? Realm()
                        try? realm?.write {
                            realm?.delete(self.model.myModels[index])
                        }
                    }
                }
            }.navigationBarItems(trailing: Button(action: {
                let myModel = MyModel()
                myModel.title = "\(Date())"
                let realm = try? Realm()
                try? realm?.write {
                    realm?.add(myModel)
                }
            }) {
                Text("Add")
            })
        }
    }
}

実行すると削除ボタンが出るようになります。

f:id:llcc:20200426203427p:plain

ただ、削除しようとすると以下のようなエラーが出てしまいます。
Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'

これはデータを削除した後もSwiftUIが削除済みデータにアクセスするからです。
今回はcellの対になるViewModelを作る事で対応しました。
ViewModelの定義は下の通りです。

struct ContentViewCellModel {
    let id: String
    let title: String
}

View全体のViewModelもContentViewCellModelの配列を持つようにします。

class ContentViewModel: ObservableObject {
    private var token: NotificationToken?
    private var myModelResults = try? Realm().objects(MyModel.self)
    @Published var cellModels: [ContentViewCellModel] = []
    
    init() {
        token = myModelResults?.observe { [weak self] _ in
            self?.cellModels = self?.myModelResults?.map { ContentViewCellModel(id: $0.id, title: $0.title) } ?? []
        }
    }
    
    deinit {
        token?.invalidate()
    }
}

Viewもこれに合わせて変更します。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.cellModels, id: \.id) { cellModel in
                    Button(action: {
                        let myModel = try? Realm().objects(MyModel.self).filter("id = %@", cellModel.id).first
                        try? Realm().write {
                            myModel?.title = "\(Date())"
                        }
                    }) {
                        Text("\(cellModel.title)")
                    }
                }.onDelete { indexSet in
                    let realm = try? Realm()
                    if let index = indexSet.first, let myModel = realm?.objects(MyModel.self).filter("id = %@", self.model.cellModels[index].id).first {
                        try? realm?.write {
                            realm?.delete(myModel)
                        }
                    }
                }
            }.navigationBarItems(trailing: Button(action: {
                let myModel = MyModel()
                myModel.title = "\(Date())"
                let realm = try? Realm()
                try? realm?.write {
                    realm?.add(myModel)
                }
            }) {
                Text("Add")
            })
        }
    }
}

これで削除しても落ちなくなりました。

f:id:llcc:20200426204652p:plain