"Core Dataを使用しない"を選択したときのAppDelegate
// Core Data 使用しない場合のAppDelegate
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return true
}
func applicationWillResignActive(_ application: UIApplication) {
}
func applicationDidEnterBackground(_ application: UIApplication) {
}
func applicationWillEnterForeground(_ application: UIApplication) {
}
func applicationDidBecomeActive(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
}
}
"Core Dataを使用する"を選択したときのAppDelegateが以下になる。手動でCore Dataを追加する場合は、以下を参考にコードを追加する。
// Core Dataを使用するときのAppDelegate
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
//
// ここまでは、Core Data を使用しないときと同じなので省略
//
func applicationWillTerminate(_ application: UIApplication) {
self.saveContext()
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
// "XXX"はプロジェクト名がデフォルトで指定される
let container = NSPersistentContainer(name: "XXX")
container.loadPersistentStores(completionHandler: {
(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
手動で、Core Dataを作成してエンティティを追加する
新規作成で、Data Modelを選択する
拡張子は*.xcdatamodeld
になる
プロジェクトにファイルが作成される
*.xcdatamodeld
を選択すると、Xcode上で専用画面に切り替わる。作成直後は空の状態である。
Add Entity
をメニューから選択して、エンティティ(データベースのテーブル相当)を追加する。
空のエンティティが作成される。Attributeの+を選択して属性(テーブルのカラム相当)を追加する。
注意点
ネットで検索すると、この後にCreate NSManagedObject Subclassを作成する手順になっているが、Xcode9でエンティティのクラスファイルを生成してビルドすると、エラーになる。
昔は、クラスファイルを生成するしかなかったらしいが、最近では選択式になって、デフォルトでは、わざわざエンティティのサブクラスファイルしなくても自動生成する。
下図がエンティティを作成した直後のデフォルト状態で、CodegenがClass Definitionになっている。このままサブクラスを生成してビルドすると、同名クラスが重複する旨のコンパイルエラーが発生する。
もしエンティティのサブクラスに、メンバ変数など追加したりカスタマイズする必要がある場合は、ソースファイルを生成する必要があるので、以下のようにCodegenの値をManula/Noneを選択すると、自動生成されなくなってコンパイルが成功するようになる。
実際にサブクラスを生成する手順が以下になる。以下のメニューからCreate NSManagedObject Subclassを選択する。
データモデルとエンティティを選択して進めると、サブクラスが生成される。
生成された各ソースファイルの中身は以下になる。NSManagedObject
を継承したエンティティクラスになっており、
// Person+CoreDataClass.swift
import Foundation
import CoreData
@objc(Person)
public class Person: NSManagedObject {
}
そのサブクラスのエクステンションとして、fetchRequest
メソッドの実装と、属性がメンバ変数になっている。
// Person+CoreDataProperties.swift
import Foundation
import CoreData
extension Person {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
return NSFetchRequest<Person>(entityName: "Person")
}
@NSManaged public var name: String?
@NSManaged public var age: Int16
@NSManaged public var gender: String?
}
Core Dataなどドキュメントの保存場所について
わかりやすい解説サイトが以下にある
とりあえず、シミュレータの場合は以下になる。実機の場合もアプリケーションディレクトリ以下は同じはずである。
- シミュレータのデバイスディレクトリ
~/Library/Developer/CoreSimulator/Devices/{各モデルのデバイスディレクトリ}
- デバイス内のアプリケーションディレクトリ
このディレクトリ配下にインストールしたアプリケーションディレクトリがある。
{デバイスディレクトリ}/data/Containers/Bundle/Application/
- デバイス内のドキュメントディレクトリ
{デバイスディレクトリ}/data/Containers/Data/Application/
この配下にドキュメントやリソース、テンポラリファイルなどを格納するフォルダがある。
> cd data/Containers/Data/Application/
> ls
Documents/ Library/ tmp/
'modelURL'に代入しているモデルのディレクトリ'XXX.momd'の場所は、
{デバイス名}/data/Containers/Bundle/Application/{アプリケーションフォルダ}/{アプリケーション.app}/XXX.momd
*.mom
とXXX.omo
どちらもバイナリデータで、VersionInfo.plist
のバイナリデータだとか、オプティマイズされたファイルだとからしい。先ほど定義したPersonエンティティの情報が格納されているCore Dataの情報ファイルなのは間違いなさそう。直接中身を見るようなものではないと思われる。
RDBMSのデータベースだと、テーブルを管理しているシステムテーブル情報のようなイメージだと思う。。
> cd {デバイス名}/data/Containers/Bundle/Application/{アプリケーションフォルダ}/XXX.app
> ls
XXX.momd
> cd XXX.momd
> ls
VersionInfo.plist XXX.mom XXX.omo
Core Dataを使ったデータの検索、追加、更新、削除など
Appleの公式ドキュメント
永続データのロード、保存などの準備
まずデータの操作の前に永続データのロード、保存などの準備を行う処理を実装する。
以下の例では、永続ストアとしてSQLITEを使用している。
SQLITEファイルの保存先は、applicationDocumentsDirectory
で取得するアプリケーションのドキュメントディレクトリを指定する。
後述するプログラム内で、content.save()
を実行すると、ファイルが存在してない場合、以下のファイルが生成される。
> cd data/Containers/Data/Application/{アプリケーションフォルダ}/Documents
> ls
sample.sqlite sample.sqlite-shm sample.sqlite-wal
サンプルでは、AppDelegateに実装している。各アプリのクラスのどこからでもアクセスしやすい場所ならどこでもいいはず。
// AppDelegate.swift
// MARK: - Core Data stack
// モデル管理オブジェクトを生成して返す
lazy var managedObjectModel: NSManagedObjectModel = {
// *.momdディレクトリのURL
let modelURL = Bundle.main.url(forResource: "XXX", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
// アプリケーションのDocumentディレクトリを返す
lazy var applicationDocumentsDirectory: NSURL = {
let urls = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)
return urls[urls.count-1] as NSURL
}()
// コーディネータを生成して返す
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// 管理オブジェクトからコーディネータを生成する
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
// アプリケーションのドキュメントディレクトリに
// SQLITEデータベースファイルを作成する。(存在していれば既存データを取得)
let url = self.applicationDocumentsDirectory.appendingPathComponent("sample.sqlite")
do {
// コーディネータに永続ストアとして設定する
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil, at: url, options: nil)
} catch {
// エラー処理
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
// コンテキストを返す
lazy var managedObjectContext: NSManagedObjectContext = {
// コンテキストを生成して、コーディネータを設定して返す
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
// 永続ストアをロードして、そのコンテナを返す
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "XXX")
container.loadPersistentStores(completionHandler: {
(storeDescription, error) in
if let error = error as NSError? {
// error
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
// コンテキストを保存する
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// error
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
データの検索、追加、更新、削除
とりあえずボタンをタップしたときに検索、追加、更新、削除を行うようなイメージで各処理を実装すると以下になる。
- 検索
コンテキストを取得してPersonのスタティックメソッドfetchRequest
でリクエストオブジェクトを生成して、フェッチしている。NSPredicate
はSQLのWhere句のような検索条件のクラスになる。
func search()->[Person]{
// コンテキストを取得
let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
let context:NSManagedObjectContext = appDelegate.managedObjectContext
let fetchRequest:NSFetchRequest<Person> = Person.fetchRequest()
// 検索条件
let predicate = NSPredicate(format:"%K = %@","name","swift 太郎")
fetchRequest.predicate = predicate
// データをフェッチ
let fetchData = try! context.fetch(fetchRequest)
return fetchData
}
- 挿入
Personクラスをインスタンス化して値を設定し、最後のコンテキストでsave()
することによって、データが追加される。SQLっぽさが隠蔽化されてなくなっている。
func insert()->Void{
// insert
let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
let context:NSManagedObjectContext = appDelegate.managedObjectContext
let person = Person(context:context)
person.name = "swift 太郎"
person.gender = "male"
person.age = 20
do{
try context.save()
}catch{
print(error)
}
return
}
- 更新
search()
でフェッチしたデータに対して、1件ずつ値を更新して、最後に保存している。ループする分、SQLよりも効率悪そうだが。。更新に関しては、ほかの方法があるかもしれないが別の機会とする。とりあえず単純なやり方のみ。
func update()->Void{
let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
let context:NSManagedObjectContext = appDelegate.managedObjectContext
// update
let data = search()
for i in 0..<data.count{
data[i].age = 19
}
do{
try context.save()
}catch{
print(error)
}
}
- 削除
search()
でフェッチしたデータに対して、1件ずつ値を削除指示する。最後にコンテキストを保存してコミットしている。
func delete()->Void{
let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
let context:NSManagedObjectContext = appDelegate.managedObjectContext
// delete
let data = search()
for i in 0..<data.count{
let deleteObject = data[i] as Person
context.delete(deleteObject)
print("delete")
}
do{
try context.save()
print("delete commit")
}catch{
print(error)
}
}