8
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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 が使えるようになります。

UseCoreData.png

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

2. 独自クラスの設定

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

memo_image.png

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

add_entity.png

3. 独自クラスの作成

メモの情報を持つクラス MemoInfo クラスを作成します。

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

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

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

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を継承させます。そうすると, encoderequired init が足りないと怒られるので次のように追加します。これを追加することで MemoInfo型をData型に変換できるようになり, Core Data で保存したり,逆に読み込んだりできるようになります1

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() で保存できます。以下のように行います。

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)
        }

    }
    ...
}

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

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 を使います。

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)
                }
            }

        }

    }
    ...
}

ここで,

managedContext.fetch(fetchRequest)

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

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

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

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

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

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" のデータだけきちんと表示されています。また,メモの内容や背景色も適切な値が表示されています(わかりにくいですが...)。

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とした場合,上記のコードを変更点する必要があるので,また調べます。まだまだ改良の余地がありそう。

全体のコード

コードはこちら


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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?