【Swift UI】Core Dataの使い方!SQLiteにデータを永続的に保存する

この記事からわかること

  • Swift UICore Data利用する方法
  • SQLite使い方
  • .xcdatamodelファイル役割定義方法
  • NSPersistentContainerクラスとは?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

公式リファレンス:Core Data

環境

Core Dataとは?

Core DataとはAppleが提供しているデータを永続的に保存、管理するためのフレームワークです。アプリ内のモデルオブジェクト(データ)をRDB(リレーショナルデータベース)で管理できる形式に変換してくれる役割があります。データベース自体の構築や付随するCRUD処理などもCore DataのAPIとして提供されており、またデータベースはデバイス内に保管されるためアプリが停止した際にもデータを保持し、再度アプリが起動した際にデータを利用できるようになります。

Core DataはバックエンドストアとしてSQLiteが活用されていますが、保存方法はカスタマイズ可能でメモリやバイナリファイルへ保存先を変更することも可能です。メモリに保存する場合はアプリ起動中のみ保持される一時的なデータになるので永続的にデータを保持したい場合はデフォルト設定のSQLiteなどを使用します。

Core Dataとは?〜まとめ〜

データを永続的に保存する他の方法

Swiftでアプリ内でデータを永続的に保存する方法は他にも存在します。

それぞれに特徴やメリットがありますが、Core Dataでは「データモデル定義の容易さとリレーションシップ」が大きなメリットだと思います。「Realm Swift」でも同じようなメリットがありますが、Core DataはApple公式のフレームワークであり、導入作業やバージョン管理が必要ないのも使い分けのポイントになると思います。

Core Dataの特徴や用語集

使い方

Core Dataを使用するためにはプロジェクト作成時に「Use Core Data」にチェックを入れる必要があります。

Xcodeの新規プロジェクト作成画面

チェックを入れた状態でプロジェクトを生成すると「プロジェクト名.xcdatamodel」と「Persistence.swift」が自動で生成されます。

プロジェクト名.xcdatamodel

「プロジェクト名.xcdatamodel」はCore Dataで利用するデータモデル(Entity)を定義するためのファイルです。エンティティーを追加するには下部にある「Add Entity」をクリックし、データに持たせたい属性(Attributes)を追加していくだけです。データベースのテーブルを作っていくようなイメージですね。

【Swift UI】Core Dataの使い方!SQLiteにデータを永続的に保存する

このファイルでエンティティー間のリレーションを定義することもできるようです。サンプルで自動生成時にItemが定義されています。

Persistence.swift

「Persistence.swift」はCore Dataのロジック部分が記述されているファイルです。これもチェックを入れることで自動生成されます。コードの役割や意味をコメントで付与しておきました。

import CoreData

struct PersistenceController {
    // シングルトン
    static let shared = PersistenceController(inMemory: true)

    // プレビュー用のデモデータ用(なくてもOK)
    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            // データの追加
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

    // 初期化&セットアップ処理」
    init(inMemory: Bool = false) {
        // .xcdatamodelファイル(モデルファイル名を渡す)
        // 永続的なコンテナー インスタンスを作成
        container = NSPersistentContainer(name: "CoreDataTest")
        
        if inMemory {
            // オンメモリで使用したい場合
            // /dev/nullはLinuxにおいてゴミ箱的な役割の特別なファイル
            print("InMemory")
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        } else {
            // falseの場合は通常通りSQLiteファイルの保存する
            print("InFile")
        }
        // 永続ストアをロード ストアが存在しない場合は自動作成
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        // 親コンテキストからの変更を自動的にマージする
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

Swift UIでの実装

Swift UIでCore Dataを操作する場合は以下のようになります。これは「タイムスタンプをCore Data内で永続化して保存して表示するサンプルコード」です。

import SwiftUI
import CoreData

struct ContentView: View {
    // コンテキストの取得
    @Environment(\.managedObjectContext) private var viewContext
    
    // データ取得
    // データの変化とともにプロパティの値も自動で変化する
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                    } label: {
                        Text(item.timestamp!, formatter: itemFormatter)
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
    }
    
    // データの追加処理
    /// エンティティをインスタンス化してデータを格納
    /// viewContext.save() で反映
    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
            
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    
    // データの削除処理
    /// データを削除する
    /// viewContext.save() で反映
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)
            
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .medium
    return formatter
}()

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

管理クラスとして切り出してみる

Swift UIで使用する場合は画面ごとに実装する形になってしまうのでCore Dataを一括で管理するクラスDataRepositoryを生成して切り分けてみたいと思います。まずは新しいエンティティを作成します。既存であったItemなどを削除して必要なエンティティだけにしておきます。

【Swift UI】Core Dataの使い方!SQLiteにデータを永続的に保存する

まずはCore Dataを操作する上で必要なNSPersistentContainerNSManagedObjectContextを用意します。


class DataRepository {
    
    private static let persistentName = "LinkMark"
    
    private static var persistenceController: NSPersistentContainer = {
        let container = NSPersistentContainer(name: DataRepository.persistentName)
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
        return container
    }()
    
    private static var context: NSManagedObjectContext {
        return DataRepository.persistenceController.viewContext
    }
}

Core Dataを使用した開発したiOSアプリがあるので参考にしてみてください。

おすすめ記事:LinkMark

エンティティクラスのインスタンス化とデータの追加

エンティティクラスをインスタンス化するには引数にNSManagedObjectContextを渡す必要があるのでDataRepository内のメソッドとして実装します。実際にデータベースに追加するためにはinsertメソッドを使用します。


/// 新規作成
static func newLocator() -> Locator {
    let entity = Locator(context: context)
    return entity
}

/// 追加処理
static func insert(_ object: NSManagedObject) {
    context.insert(object)
}

汎用的なエンティティインスタンスメソッドを切り出す

ジェネリクスを活用することで汎用的なインスタンス生成メソッドを切り出すことも可能です。エンティティが複数ある場合はこちらの方が使いやすいかもしれません。

/// 新規作成
static func entity<T: NSManagedObject>() -> T {
    let entityDescription = NSEntityDescription.entity(forEntityName: String(describing: T.self), in: context)!
    return T(entity: entityDescription, insertInto: nil)
}

データを取得する

データベースからデータを取得するにはNSFetchRequestを使用します。


/// 取得処理
static func fetch() -> [Locator] {
    let fetchRequest = NSFetchRequest<Locator>(entityName: String(describing: Locator.self))
    do {
        return try context.fetch(fetchRequest)
    } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
        return []
    }
    
}

データを更新する

データベースのデータを更新するには対象のデータのみを取得して、変更したいプロパティをそのまま上書きして保存するだけです。DataRepositoryクラスに1つだけ取り出すfetchSingleメソッドを追加しておけば簡単に更新処理が行えます。

public func updateCategory(categoryId id : UUID, name: String, color: String) {
    let predicate = NSPredicate(format: "id == %@", id as CVarArg)
    guard let category: Category = DataRepository.fetchSingle(predicate: predicate) else { return }
    
    category.name = name
    category.color = color
    
    DataRepository.save()
}

データを削除する

データベースからデータを削除するにはdeleteメソッドを使用します。


/// 削除処理
static func delete(_ object: NSManagedObject) {
    context.delete(object)
}

変更を保存する

データを追加や削除した場合はsaveする必要があります。エラーを投げる可能性があるのでdo-catch文でエラーを補足します。


/// 保存処理
static func save() {
    // 変更がある場合のみ
    guard context.hasChanges else { return }
    do {
        try context.save()
    } catch let error as NSError {
        fatalError("Unresolved error \(error), \(error.userInfo)")
    }
}

保存先のファイルを指定する

Core Dataで保存先のファイルを変更するには保存したいデータベースのURLを作成し、NSPersistentStoreDescription(url:)の引数に渡してpersistentStoreDescriptionsプロパティに渡してロードするだけです。

// 新しいデータベースのURLを指定
let newStoreURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("データベース名.sqlite")

// NSPersistentStoreDescriptionを作成し、URLを設定
let storeDescription = NSPersistentStoreDescription(url: newStoreURL)

// NSPersistentContainerを作成し、NSPersistentStoreDescriptionをセット
let container = NSPersistentContainer(name: "LinkMark")
container.persistentStoreDescriptions = [storeDescription]

// コンテナをロードします
container.loadPersistentStores { (storeDescription, error) in
    if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index