CoreData で独自クラスのデータを保存する

# データの保存
メモアプリなどでは，アプリケーションを一度終了した後もメモのデータをアプリ内に保存しておく必要があります。再度起動した際にそのデータを再び読み込みます。
データを保存するには以下の方法があります。

1. Userdefaults の使用
2. Core Data の使用
3. Keychain の使用

今回は，2番のCoreDataの使用について見ていきたいと思います。

# 環境
Swift 5.1
Xcode 11.0

# メモアプリの作成
TestTaskApp001という名前でアプリケーションを作っていきます。Core Data の利用に着目したいため，**UIは追求しません**（見やすいようにTableViewなどを使うとその扱いが入ってきて紛らわしいため）。標準出力にメモの内容を表示するだけにします。

## 1. Core Data の使用
早速アプリケーションを作っていきます。最初の Product Name を記入する段階で， `Use Core Data` にチェックを入れます。これにより Core Data が使えるようになります。

<img width="500" alt="UseCoreData.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297735/6f35ab03-505f-3b8b-04cc-4152f444507b.png">

`Use Core Data` にチェックを入れると，TestMemoApp001.xcdatamodeldというファイルが生成されます。ここに，保存したいデータのEntityを追加して行くことになります。

## 2. 独自クラスの設定
メモを全て同列で管理することも可能ですが，メモの内容ごとにグループでまとめることを考えます。このグループの単位を `MemoGroup` というクラスで管理します。

![memo_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297735/6cd70a03-3ff2-cdce-875c-0888e6400c04.png)


Entityに`MemoGroup`を追加します。その後，Attributeを追加します。Attributeとは属性の意味で，ここではクラスの概念におけるフィールドだと思ってください。ここでは， MemoGroupの名前（ `groupName` ）とメモの内容（ `memosData` ）を追加します。groupNameのTypeをStringに， memosDataのTypeをBinary Dataに設定します。これは，データをData型やNSData型で扱うものです。

<img width="1397" alt="add_entity.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297735/88f4df5b-7d63-3961-d420-c32d5cd352c7.png">

## 3. 独自クラスの作成
メモの情報を持つクラス `MemoInfo` クラスを作成します。

```swift:MemoInfo.swift
class MemoInfo: NSObject {
    var detail: String
    var color: UIColor
    static let defaultColor: UIColor = .white
    
    override init() {
        self.detail = ""
        self.color = MemoInfo.defaultColor
    }
}
```

|変数名|変数の説明|
|:-:|:-:|
| `detail` |メモの文章|
| `color` | `TableView` などで表示する場合のメモの背景色。重要なメモは赤色といったように（ `CGColor` 型でも良いかも）。|
| `defaultColor` | `color` の初期値（クラス変数）。`color` が指定されなかった場合のメモの背景色。|

![memoInfo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297735/9f17117b-f583-6383-c241-9a76f183f035.png)


Core Data でも本当は次のようなクラスでデータを保存したいはずです。

```swift:
// このコードを作成する必要はない

class MemoGroup: NSObject {
    var groupName: String
    var memosData: [MemoInfo]
    
    override init() {
        self.groupName = "No Name"
        self.memosData = []
    }
}
```

しかし， Core Data ではMemoInfo型のような独自クラスのデータを直接扱うことはできません。取り扱えるData型に変換してやる必要があります。

MemoInfo型を Core Data で取り扱えるように，MemoInfoクラスにNSCodingを継承させます。そうすると， `encode` と `required init` が足りないと怒られるので次のように追加します。これを追加することで MemoInfo型をData型に変換できるようになり， Core Data で保存したり，逆に読み込んだりできるようになります[^1]。

```swift:MemoInfo.swift
class MemoInfo: NSObject, NSCoding {
    var detail: String
    var color: UIColor
    static let defaultColor: UIColor = .white
    
    override init() {
        self.detail = ""
        self.color = MemoInfo.defaultColor
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(self.detail, forKey: "detail")
        coder.encode(self.color, forKey: "color")
    }
    
    required init?(coder: NSCoder) {
        self.detail = coder.decodeObject(forKey: "detail") as! String
        self.color = coder.decodeObject(forKey: "color") as! UIColor
    }
    
}
```

## 4. Core Data にデータを保存

NSManagedObjectContext型の `managedContext` を取得して， `managedContext.save()` で保存できます。以下のように行います。

```swift:ViewController.swift
class ViewController: UIViewController {
    ...
    func saveMemos(memoGroupName: String, memoInfos: [MemoInfo]) {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        
        let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext
        
        do {
            let memoGroup: MemoGroup = MemoGroup(context: managedContext)
            let memosData: Data = try NSKeyedArchiver.archivedData(withRootObject: memoInfos, requiringSecureCoding: false) as Data
            memoGroup.memosData = memosData
            memoGroup.groupName = memoGroupName
        } catch let error {
            print(error.localizedDescription)
        }
        
        do {
            try managedContext.save()
        } catch let error {
            print(error.localizedDescription)
        }
        
    }
    ...
}
```

独自クラスを扱う場合の重要な点は，

```swift:ViewController.swift
let memosData: Data = try NSKeyedArchiver.archivedData(withRootObject: memoInfos, requiringSecureCoding: false) as Data
```

の部分です。この部分で，MemoInfo型の配列をData型に変換しています。MemoInfo型をData型に変換できるようにMemoInfoクラスを作っておくことで，MemoInfo型の配列もData型に変換できるようになります。逆にMemoInfoクラスがData型に変換できないとこの部分でエラーが生じます。

## 5. Core Data からデータを取得
Core Data からデータを読み込む場合は， `NSFetchRequest` を使います。 

```swift:ViewController.swift
class ViewController: UIViewController {
    ...
    func readMemos(memoGroupName: String) {
        var memoGroups: [MemoGroup] = []
        
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        
        let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext
        let fetchRequest: NSFetchRequest<MemoGroup> = MemoGroup.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "%K = %@", "groupName", memoGroupName)
        
        do {
            memoGroups = try managedContext.fetch(fetchRequest)
        } catch let error {
            print(error.localizedDescription)
        }
        
        for memoGroup in memoGroups {
            if let groupName = memoGroup.groupName, let memosData = memoGroup.memosData {
                print(groupName)
                do {
                    let data: Data = memosData as Data
                    let memosInfo: [MemoInfo] = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [MemoInfo]
                    for memoInfo in memosInfo {
                        print("detail: \(memoInfo.detail), color: \(memoInfo.color.description), defaultColor: \(MemoInfo.defaultColor.description)")
                    }
                } catch let error {
                    print(error.localizedDescription)
                }
            }
            
        }
        
    }
    ...
}
```

ここで，

```swift:
managedContext.fetch(fetchRequest)
```

により，MemoGroupの配列が返ってきます。また，

```swift:ViewController.swift
fetchRequest.predicate = NSPredicate(format: "%K = %@", "groupName", memoGroupName)
```

の部分で，条件に合うデータのみを取り出すことができます。ここでは， `groupName` が `memoGroupName` に一致するものだけを取り出すことになります。

## 6. データの保存と取得の確認

次のように，データを作成し，保存→読みこみをしてみます。

```swift:ViewController.swift
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var memoInfos: [MemoInfo] = []
        var memoGroupName: String = "Work"
        
        let memoInfo0 = MemoInfo()
        memoInfo0.detail = "12:00~ @Ikebukuro"
        memoInfos.append(memoInfo0)
        
        let memoInfo1 = MemoInfo()
        memoInfo1.color = .red
        memoInfo1.detail = "13:00~ @Shibuya"
        memoInfos.append(memoInfo1)
        
        // Save "Work" memo group
        saveMemos(memoGroupName: memoGroupName, memoInfos: memoInfos)
        
        memoInfos = []
        memoGroupName = "Study"
        
        let memoInfo2 = MemoInfo()
        memoInfo2.detail = "20:30~ : Test Preparation"
        memoInfos.append(memoInfo2)
        
        // Save "Study" memo group
        saveMemos(memoGroupName: memoGroupName, memoInfos: memoInfos)
        
        readMemos(memoGroupName: "Work")
    }
    ...
}
```

出力結果は次のようになります。groupNameが `"Work"` のデータだけきちんと表示されています。また，メモの内容や背景色も適切な値が表示されています（わかりにくいですが...）。

```shell:
Work
detail: 12:00~ @Ikebukuro, color: UIExtendedGrayColorSpace 1 1, defaultColor: UIExtendedGrayColorSpace 1 1
detail: 13:00~ @Shibuya, color: UIExtendedSRGBColorSpace 1 0 0 1, defaultColor: UIExtendedGrayColorSpace 1 1
```

# Core Data における独自クラスの扱い方のまとめ
1. AttributeのTypeはBinary Dataとする。
2. 独自クラスはcoderを利用してData型に変換できるようにする。
3. データの保存は， `NSKeyedArchiver.archivedData` を利用して，独自クラスの型からData型へ変換する必要がある。
4. データの取得は， `NSKeyedUnarchiver.unarchiveTopLevelObjectWithData` を利用してData型から独自クラスの型へ変換する必要がある。

今回はAttributeのTypeをBinary Dataとしましたが，Transformableとする方法もあるようです。Transformableとした場合，上記のコードを変更点する必要があるので，また調べます。まだまだ改良の余地がありそう。

# 全体のコード
コードは[こちら](https://github.com/tky1117/TestMemoApp/)

[^1]: defaultColorをエンコード・デコードしていないのは，この値をCore Dataに保存しない前提で考えているからです。メモの背景色をユーザー設定で決める場合などは，別途Entityを用意して同じような手順を踏む必要があります。