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

【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とは?〜まとめ〜

NSManagedObjectContext

Core Dataの肝となるのがNSManagedObjectContextの存在です。NSManagedObjectContextCRUD処理を行うための処理を主に提供していますが、実際にデータが永続化されるのはNSManagedObjectContext.saveメソッドを呼び出したタイミングです。

NSManagedObjectContextデータの状態変化を一時的にプールして管理している場所であり、NSManagedObjectContext.saveメソッドを呼ばないとNSManagedObjectContextに蓄積したデータの変更永続化されないのでアプリを落として起動し直すと前の状態のままになってしまうので注意してください。

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

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

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

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

Core Dataの特徴や用語集

Swift UIでの使い方

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

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

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

プロジェクト名.xcdatamodel

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

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

このファイルでエンティティー間のリレーションなども定義することが可能です。

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

マルチスレッドで扱うなら注意

Core DataはスレッドセーフではないのでContextを異なるスレッドから参照するとアプリがクラッシュしてしまう可能性があります。マルチスレッドで扱うためにはスレッドごとにContextを用意しておくのがベターなのでアプリの設計の段階でメインのみなのかバックグラウンドスレッドからも操作することがありえるのかを考慮しておく必要があります。

Core Dataを活用したデモプロジェクトも作成してみたのでよかったら参考にしてください。

おすすめ記事:MyCoreDataTest

Contextの変更を保存(永続化)する

Contextにデータの追加や更新、削除などをした場合saveメソッドを実行する必要があります。これでデータが永続化されます。hasChangesメソッドでそもそもContext変更があったかどうかを識別することも可能です。またsaveメソッドはエラーを投げる可能性があるのでdo-catch文でエラーを補足します。

/// 保存処理
private func saveContext(_ context: NSManagedObjectContext) {
        guard context.hasChanges else { return }
        do {
            try context.save()
        } catch let error as NSError {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    }

エンティティクラスのインスタンス化の方法

エンティティクラス(NSManagedObject)をインスタンス化するにはイニシャライザの引数にNSManagedObjectContextを渡す必要があります。ここでContextを渡すことで対象オブジェクトとContextが紐づき、インスタンス化時にContextに追加、プロパティ変更時にもContextにその変更が反映されます。

/// 新規作成
let newItem = Item(context: viewContext)
newItem.timestamp = Date()

エンティティクラスをインスタンス化する方法は他にいくつか存在します。その中にはContextと紐付けない方法などもあるので詳細は以下の記事を参考にしてください。

データを追加する

先ほど紹介した方法ではインスタンスがContextと紐づいていたので不要ですが、Contextと紐づいていないオブジェクトを生成した場合は明示的にContextに追加する必要があります。追加はinsertメソッドで行うことが可能です。

/// 追加処理
context.insert(object)

データを取得する

データベースからデータを取得するにはSwift UIの場合は@FetchRequestを付与するだけで簡単に実装することが可能です。ソートやフィルタリングも行うことが可能です。

@FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
private var items: FetchedResults

Swift UIを使用したくない場合はNSFetchRequestを使用します。

public func fetch() -> [Person] {
    // String(describing: Person.self) → "Person" と同じ
    let fetchRequest = NSFetchRequest<Person>(entityName: String(describing: Person.self))
    do {
    return try context.fetch(fetchRequest)
    } catch let error as NSError {
      print("Could not fetch. \(error), \(error.userInfo)")
      return []
    }
}

ジェネリックを使用して汎用的なfetchメソッドを実装することも可能なので詳細は以下の記事を参考にしてください。

データを削除する

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

context.delete(object)

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

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