SwiftUIでRealmを使ってみた

##はじめに
SwiftUIでRealmを使ったサンプルアプリを作成しました。
理解するのに時間がかかったので、自分のアウトプットを兼ねて書いています。
↓こんな動きをするアプリです。右の数字は0~100からランダムで取得します。
<img width="250" alt="ezgif.com-video-to-gif.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406612/401304b9-97e2-9c53-5c07-d3b78be78ce2.gif">


##環境
Swift 5.3
Xcode 12.0.1
Cocoapods 1.9.3
RealmSwift 5.4.7

##経緯
最近、SwiftでRealmに初めて触れ、SwiftUIではどのように実装できるのか気になったので調べ始めました。

##実装した動作

・追加ボタン
　Listに「First Title」と0〜100の乱数の行を追加
・ゴミ箱マーク
　対応する行を削除
・全削除ボタン
　行を全て削除
・行のタイトルを長押し
　「First Title」を「Changed Title」にリネーム、乱数再取得
・Realmで保存
　アプリを落としてもデータ保存

##ソースコード

まずはデータの元となるコードを記述します。

```swift:ItemDB.swift
import RealmSwift
// Realm用のClass
class ItemDB: Object {
  @objc dynamic var id = 0
  @objc dynamic var title = ""
  @objc dynamic var number = 0
  
  // 主キーを使うと、データの更新や削除に便利
  override static func primaryKey() -> String? {
    "id"
  }
}
```

```swift:Item.swift
import Foundation

struct Item: Identifiable {
  let id: Int
  let title: String
  let number: Int
}

extension Item {
  init(itemDB: ItemDB) {
    id = itemDB.id
    title = itemDB.title
    number = itemDB.number
  }
}
```

```swift:ItemStore.swift
import RealmSwift

final class ItemStore: ObservableObject {

  private var itemResults: Results<ItemDB>
  
  // itemResultsにDBのデータをセット
  init(realm: Realm) {
    itemResults = realm.objects(ItemDB.self)
  }
  
  var items: [Item] {
    itemResults.map(Item.init)
  }
}
```

ItemStore.swiftには、DBを操作する以下のメソッドも記述します。
上記の実装した動作を行うためのものです。

- create （データを追加）
- update （データを更新）
- delete （データを削除）
- deleteAll （データを全削除）

```swift:ItemStore.swift
extension ItemStore {
  // データを追加
  func create() {
    // これを書かないとDBの変更をViewに伝えることができません。
    objectWillChange.send()
    
    do {
      let realm = try Realm()
      let itemDB = ItemDB()
      itemDB.id = UUID().hashValue
      itemDB.title = "First Title"
      itemDB.number = Int.random(in: 0...100)
      try realm.write {
        realm.add(itemDB)
      }
    } catch let error {
      print(error.localizedDescription)
    }
  }
  
  // データを更新
  func update(itemID: Int) {
    objectWillChange.send()

    do {
      let realm = try Realm()
      try realm.write {
        realm.create(ItemDB.self,
                     value: ["id": itemID, 
                             "title": "Changed Title", 
                             "number": Int.random(in: 0...100)],
                     update: .modified)
      }
    } catch let error {
      print(error.localizedDescription)
    }
  }
  
  // データを削除
  func delete(itemID: Int) {
    objectWillChange.send()
    
    guard let itemDB = itemResults.first(where: { $0.id == itemID})
    else {
      return
    }
    
    do {
      let realm = try Realm()
      try realm.write {
        realm.delete(itemDB)
      }
    } catch let error {
      print(error.localizedDescription)
    }
  }
  
  // データを全削除
  func deleteAll() {
    objectWillChange.send()
    
    do {
      let realm = try Realm()
      try realm.write {
        realm.deleteAll()
      }
    } catch let error {
      print(error.localizedDescription)
    }
  }
}

```


続いてViewとなるコードを書いていきます。

```swift:ItemRowView.swift
import SwiftUI

struct ItemRowView: View {
  @EnvironmentObject var store: ItemStore
  let item: Item
  
  var body: some View {
    HStack{
      Text(item.title)
        // タイトルの長押しでデータの更新
        .onLongPressGesture {
          store.update(itemID: item.id)
        }
      Spacer()
      Text(String(item.number))
      // ゴミ箱マークを設置して、削除を実装
      Image(systemName: "trash.circle.fill")
        .resizable()
        .frame(width: 24, height: 24)
        .foregroundColor(.red)
        .onTapGesture {
          store.delete(itemID: item.id)
        }
    }
  }
}
```


```swift:ItemListView.swift
import SwiftUI

struct ItemListView: View {
  @EnvironmentObject var store: ItemStore
  let items: [Item]
  
  var body: some View {
    List {
      Section(header: sectionHeaderView) {
        ForEach(items) { item in
          HStack{
            ItemRowView(item: item)
          }
        }
      }
    }
    .navigationTitle("RealmDBリスト")
  }
  
  // ヘッダーに追加ボタンと全削除ボタンを設置
  var sectionHeaderView: some View {
    HStack {
      Button("追加", action: store.create)
      Spacer()
      Button("全削除", action: store.deleteAll)
    }
  }
}
```

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
  @EnvironmentObject var store: ItemStore
  
  var body: some View {
    NavigationView {
      ItemListView(items: store.items)
    } 
  }
}
```

最後に、「SceneDelegate.swift」の一部を書き換えて完了です。

```swift:SceneDelegate.swift
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)
      // Realmを最初に読み込むところ
      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)")
    }      
  }
}
```

**ソースコード全体はこちら**
https://github.com/takuma-2531/SwiftUI_Realm_1To1


##参考にしたサイト
https://www.raywenderlich.com/12235561-realm-with-swiftui-tutorial-getting-started
SwiftUIでのRealmの使い方を調べた中で、このサイトが一番わかりやすかったです。
英語ですが、Google翻訳を駆使して読みました。
このサイトのコードをより単純にしたものが上記サンプルです。

##おわりに
最後まで読んでくださりありがとうございます。
SwiftUIやRealmを使う人の役に立てれば幸いです。