データの保存
メモアプリなどでは,アプリケーションを一度終了した後もメモのデータをアプリ内に保存しておく必要があります。再度起動した際にそのデータを再び読み込みます。
データを保存するには以下の方法があります。
- Userdefaults の使用
- Core Data の使用
- Keychain の使用
今回は,2番のCoreDataの使用について見ていきたいと思います。
環境
Swift 5.1
Xcode 11.0
メモアプリの作成
TestTaskApp001という名前でアプリケーションを作っていきます。Core Data の利用に着目したいため,UIは追求しません(見やすいようにTableViewなどを使うとその扱いが入ってきて紛らわしいため)。標準出力にメモの内容を表示するだけにします。
1. Core Data の使用
早速アプリケーションを作っていきます。最初の Product Name を記入する段階で, Use Core Data
にチェックを入れます。これにより Core Data が使えるようになります。
Use Core Data
にチェックを入れると,TestMemoApp001.xcdatamodeldというファイルが生成されます。ここに,保存したいデータのEntityを追加して行くことになります。
2. 独自クラスの設定
メモを全て同列で管理することも可能ですが,メモの内容ごとにグループでまとめることを考えます。このグループの単位を MemoGroup
というクラスで管理します。
EntityにMemoGroup
を追加します。その後,Attributeを追加します。Attributeとは属性の意味で,ここではクラスの概念におけるフィールドだと思ってください。ここでは, MemoGroupの名前( groupName
)とメモの内容( memosData
)を追加します。groupNameのTypeをStringに, memosDataのTypeをBinary Dataに設定します。これは,データをData型やNSData型で扱うものです。
3. 独自クラスの作成
メモの情報を持つクラス MemoInfo
クラスを作成します。
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 が指定されなかった場合のメモの背景色。 |
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を継承させます。そうすると, encode
と required init
が足りないと怒られるので次のように追加します。これを追加することで MemoInfo型をData型に変換できるようになり, Core Data で保存したり,逆に読み込んだりできるようになります1。
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()
で保存できます。以下のように行います。
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)
}
}
...
}
独自クラスを扱う場合の重要な点は,
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
を使います。
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の配列が返ってきます。また,
fetchRequest.predicate = NSPredicate(format: "%K = %@", "groupName", memoGroupName)
の部分で,条件に合うデータのみを取り出すことができます。ここでは, groupName
が memoGroupName
に一致するものだけを取り出すことになります。
6. データの保存と取得の確認
次のように,データを作成し,保存→読みこみをしてみます。
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 における独自クラスの扱い方のまとめ
- AttributeのTypeはBinary Dataとする。
- 独自クラスはcoderを利用してData型に変換できるようにする。
- データの保存は,
NSKeyedArchiver.archivedData
を利用して,独自クラスの型からData型へ変換する必要がある。 - データの取得は,
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData
を利用してData型から独自クラスの型へ変換する必要がある。
今回はAttributeのTypeをBinary Dataとしましたが,Transformableとする方法もあるようです。Transformableとした場合,上記のコードを変更点する必要があるので,また調べます。まだまだ改良の余地がありそう。
全体のコード
コードはこちら
-
defaultColorをエンコード・デコードしていないのは,この値をCore Dataに保存しない前提で考えているからです。メモの背景色をユーザー設定で決める場合などは,別途Entityを用意して同じような手順を踏む必要があります。 ↩