SwiftUI - CoreDataを調べてみる

1. CoreDataを調べてみる

alt

CoreDataを使用するには新規プロジェクト作成時に、Use Core Dataにチェックを入れてからプロジェクトを作成します。そうすると下記のようなファイルがデフォルトで作成されます。ここではCoreDataを理解するために下記のファイルの中身を調べてみようと思います。

  • AppName.swift
  • ContentView.swift
  • Persistence.swift
  • AppName.xcdatamodeld

2. 「Cannot find type 'XXX' in scope」のエラー解決

スコープ内にタイプ「XXX」が見つかりません。というようなエラーが出たときは下記の手順をすれば修正できます。

  • Xcodeを再起動
  • Shift + command + K (クリーンビルド)
  • command + B (ビルド)

3. テンプレートの修正

CoreDataのテンプレートは、現在の日時をリスト形式で出力するというものです。コードには、そのリストに項目を追加したり削除するボタンが含まれていますが、テンプレートの修正をしないと、そのボタンはシュミレータでは表示されません。Xcode 12 の SwiftUI + Core Data のプロジェクトテンプレートが不完全という記事を書いてくれている方がいて、その記事のように修正すると表示できます。(Xcode Version 12.5で検証しました)

  • NavigationView {...}の追加
  • ToolbarItem(placement: x) {...}を2箇所追加。xは、.navigationBarLeading.navigationBarTrailing
var body: some View {
    //vv追加
    NavigationView {
      List {
        ForEach(items) { item in
          Text("Item at \(item.timestamp!, formatter: itemFormatter)")
        }
        .onDelete(perform: deleteItems)
      }
      .toolbar {
        #if os(iOS)
        //vv追加 .navigationBarLeading
        ToolbarItem(placement: .navigationBarLeading) {
          EditButton()
        }
        #endif
        //vv追加 .navigationBarTrailing
        ToolbarItem(placement: .navigationBarTrailing) {
          Button(action: addItem) {
            Label("Add Item", systemImage: "plus")
          }
        }
      }
    }
  }

4. AppName.xcdatamodeld

alt

AppName.xcdatamodeldの中身を見てみると下記のようになっています。

Entity: Item
Attribute: timestamp,
Type: Date

5. Persistence.swift

このファイルだけではないですが、下記のようなコメントアウトが数カ所に書かれています。

fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

fatalError()により、アプリケーションはクラッシュログを生成して終了します。この機能は、開発中に役立つ場合がありますが、出荷アプリケーションでは使用しないでください」とのことなので忘れないようにします。

import CoreData

struct PersistenceController {
  static let shared = PersistenceController()
  
  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) {
    container = NSPersistentContainer(name: "Test") //<<プロジェクト名
    if inMemory {
      container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    }
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {

        //ここでのエラーの一般的な理由は次のとおりです。
        //*親ディレクトリが存在しないか、作成できないか、書き込みが許可されていません。
        //*デバイスがロックされている場合のアクセス許可またはデータ保護のため、永続ストアにアクセスできません。
        //*デバイスの容量が不足しています。
        //*ストアを現在のモデルバージョンに移行できませんでした。
        //エラーメッセージを確認して、実際の問題が何であったかを確認してください。
        
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
  }
}

inMemory - メモリの読み取りとメモリの書き込みのみを行う
NSPersistentContainer - アプリのCoreDataスタックをカプセル化するコンテナ
persistentStoreDescriptions - 永続コンテナによって使用される永続ストアの1つ
loadPersistentStores - 永続ストアをロード
fatalError() - 指定されたメッセージを無条件に出力し、実行を停止

6. AppName.swift

import SwiftUI

@main
struct TestApp: App {
  let persistenceController = PersistenceController.shared
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(\.managedObjectContext, persistenceController.container.viewContext)
    }
  }
}

managedObjectContext - 管理対象オブジェクトが登録されている管理対象オブジェクトコンテキスト

7. ContentView.swift

下記のコードはデフォルトではなくテンプレートを修正したものです。

import SwiftUI
import CoreData

struct ContentView: View {
  @Environment(\.managedObjectContext) private var viewContext
  
  //CoreDataからデータを取得
  @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
          Text("Item at \(item.timestamp!, formatter: itemFormatter)")
        }
        .onDelete(perform: deleteItems)
      }
      .toolbar {
        #if os(iOS)
        ToolbarItem(placement: .navigationBarLeading) {
          EditButton()
        }
        #endif
        ToolbarItem(placement: .navigationBarTrailing) {
          Button(action: addItem) {
            Label("Add Item", systemImage: "plus")
          }
        }
      }
    }
  }
  
  //CoreDataデータ追加
  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)")
      }
    }
  }
  //CoreDataデータ削除
  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)
  }
}

FetchRequest - CoreDataストアから結果を取得するプロパティラッパー
onDelete(perform:) - 動的ビューの削除アクションを設定
IndexSet - 別のコレクション内の要素のインデックスを表す一意の整数値のコレクション

Core Data | Apple Developer Documentation
NSSortDescriptor | Apple Developer Documentation
NSPersistentContainer | Apple Developer Documentation
NSManagedObjectContext | Apple Developer Documentation
init(entity:sortDescriptors:predicate:animation:) | Apple Developer Documentation
loadPersistentStores | Apple Developer Documentation
fatalError(_:file:line:) | Apple Developer Documentation
withAnimation(::) | Apple Developer Documentation