LoginSignup
14
5

More than 3 years have passed since last update.

Xcodeで、既存のプロジェクトにCore Dataを追加した

Last updated at Posted at 2021-02-27

はじめに

iOSアプリを途中まで作っていて、あとからCore Dataを追加した。

  • Core Dataをつかうこと自体がはじめて
  • Core Dataを後から追加するのはいろいろやることがある

の2点でハマったけれど、なんとかなったので記録する。

環境

下記のバージョンで実施。

  • iOS 14.4
  • Xcode 12.4
  • macOS BigSur 11.1

主な結論

  • 設定値の保管はUserDefaultsもしくはAppStorageでよいが、あるていどのデータになるとCore Dataをつかうべき

  • CoreDataの構成を変えたとき(EntityもしくはAttribute)は、アップデートする必要がある

  • @EnvironmentObjectを使うときとは違って、@Environment(\.managedObjectContext)を使ってFetchしたデータの、要素をForEachViewにわたすと、値渡しではなく参照渡しになる

手順

Core Dataの追加

  • すでにプロジェクトがあって、Core Dataを追加したい場合、File > New > File...CoreData > Data Modelを選択

  • 次に、EntityAttributeを追加する。

  • クラス定義ファイルの生成

    • EntityInspector > Data Model Inspectorパネルで、Entity > CodegenManual/Noneに変更
    • メニューからEditor > Create NSManagedObject subclass...を選択し、クラス定義ファイルを生成する

Core Dateのデータベース(っていうのかな?)の追加はこれで完了。

Core Dataのデータベースのアップデート時の操作

  • まず、メニューからEditor > Add Model Version...をクリックし、指示に従う

  • 次に、InspectorFile Inspector > Core Data Model > Model Versionで新しく作ったものを選んでおく

これをしておかないと、アプリがクラッシュする(した)。どうやら、データベースのEntityAttributeを変更したら、これをやっておかないといけない。

リソース

ここまでについて、画像入りの解説記事は、たとえば下記に詳しい:
iOSにユーザーデータを保存する方法と、そのためのコードの書き方: UserDefaults、Core Data、Key Chain、CloudKit - Qiita

プロジェクト作成時に、Life CycleでSwiftUI Appを選択していると、AppDelegate.swiftが存在しない。上記記事で、AppDelegate.swiftに関する箇所は読み飛ばした。代わりにやったことは、下記に記す。

追加するコード

いろいろ試したが、けっきょく「Core Dataつきの新規プロジェクトをつくって、ファイルや要素をコピーしてくる」のが手間が少ない気がする。

Persistence.swiftのコピーと修正

  • 新規プロジェクトを作成し、その際にUse Core Dataにチェックを入れる

  • Persistence.swiftをコピーしてくる

手で変更を要する箇所は、

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

newItem.timestamp = Date()のところ。.timestampDate()をじぶんのデータベースのAttributeにあわせる。ここは、プレビュー用のテストデータセットなので、値は適当でよい。

それと、

init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "anotherNewCoreData")
    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    }

(name: "anotherNewCoreData")を、じぶんのデータベースのEntityの名前にあわせる。

@mainについて

  • 新規プロジェクトをパクる。具体的には
let persistenceController = PersistenceController.shared
ContentView()
    .environment(\.managedObjectContext, persistenceController.container.viewContext)

の2箇所。

Content Viewについて

  • ここからは個別対応になってきて一般化しがたいが、主には下記。
@Environment(\.managedObjectContext) private var viewContext

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

のところ。@Environmentを宣言し、@FetchRequestで取りかたを指定し、itemsに格納する。

ItemsはEntityの名称に置き換え、itemsは自分の使いたい変数名にした。

private func addItem() { ... }
private func deleteItems(offsets: IndexSet) { ... }

のところは、呼び方から書き方(Core Dataへの保存のしかた)まで、流用した。

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

はプレビューの使いかた。

小ビューに取得したデータベースのレコードをわたす方法が未だにわからない...

データのわたしかたと操作

親ビューでたとえば

NavigationView{
    List {
        ForEach(records) {record in
            RecordRow(record: record)
        }
    }

と、recordを下層ビューに渡したとする。受け取った下層ビューは

Image(systemName: "checkmark.seal")
    .onTapGesture {
        record.isDone.toggle()

と書くだけで、タップするトグルされる。

@EnvironmentObjectを使ってrecordsを共有した場合、ModelData.recordsはたしかに共有されているが、ビューに渡したrecordは共有されていない(値渡し)になるので、

ModelData.records[index].isDone.toggle()

などとしないといけない。ところが、@Environment(\.managedObjectContext)の場合は、参照渡しになっているので、直接書き換えられる。

と理解しているのだけれど違っていたらご指摘ください……

保存のおまけ

新規プロジェクトからの流用ではないが、下記をやっておくとたしかに便利。

ホームボタンを押したときや、アプリを終了したときに、Core Dateに保存しておいてくれる

Persistence.swift

struct PersistenceController {

    ...

    func save() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Show some error here
            }
        }
    }
}

と書いておき、@main

@main
struct xxxxxxApp: App {
    let persistenceController = PersistenceController.shared
    @Environment(\.scenePhase) var scenePhase

    var body: some Scene {
        WindowGroup { ... }
        .onChange(of: scenePhase) { _ in
            persistenceController.save()
        }
    }
}

これは
How to configure Core Data to work with SwiftUIより。

英語だし、ちょっと説明が飛んでいるところもあるけれど、新しい情報なのですごく助かった。

その他リソース

Add, Delete & Save in Core Data in SwiftUI にもずいぶん助けられた。

カピ通信 - SwiftUI もとてもわかりやすい。残念ながら、発見したのが遅かった。最初に見つけていれば、もっとラクだったのになあ!

おわりに

けっこう悩んだけど、動くとおもしろい!!
ネット上の各種リソースに深謝🙏

14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5