🕊️

SwiftDataの前にCore Dataを学ぶ

2023/07/09に公開

前書き

こんにちは、最近visionOSとWidgetの可能性を感じつつ、中々どういった遊び方があるか思い浮かばないNaoRandDです😇

WWDC23でSwiftDataが登場し、その実は "SwiftUI用に扱いやすくしたCore Data" だと聞いたとき、Core Dataがどんなものだったっけ?となってしまったので、復習もかねて記事を書いてみようかと思います
https://developer.apple.com/xcode/swiftdata/

Core Dataとは?

簡単にいうとデータを管理するフレームワークです
https://developer.apple.com/documentation/coredata

データベースには SQLite が使われています

SQLiteとは?

SQLiteとは何ぞやということにも触れておきます

SQLiteはオープンソースで継承なRDBMS(データベース管理システム) です

SQLとついていますが、データベースを操作する言語ではなく、データベースシステム自体を指す言葉です

サーバーとしてでなく、アプリケーションに組み込むことで利用します
データベースの内容は全てローカルファイルに保存されるので、AndroidやiOSでのアプリケーションにも利用されているわけですね

参考:https://www.sophia-it.com/content/SQLite

登場人物

  • Container
    • NSPersistentContainer
    • アプリ内の Core Data スタックをカプセル化するコンテナ
    • アプリとデータベース(SQLite)の橋渡しをしてくれる役割
  • Model
    • NSManagedObjectModel
    • .xcdatamodeldオブジェクト(実際に試すときのModelファイルに該当)を記述するファイルをプログラム的に表現したもの
  • Entity
    • NSEntityDescription
    • Model内にあるユーザーが定義するEntity
    • Attribute(属性)と合わせて型情報など具体的にどういったデータを扱うかを含んだもの
  • Context
    • NSManagedObjectContext
    • データの検索や、生成、更新、削除などのCRUD処理やその追跡をするもの
  • Store
    • NSPersistentStore
    • SQLite(データベース)の情報を管理するクラス
    • coordinator経由でModel内で定義されているEntityのインスタンス実体化などを行う

それぞれの役割は下記のような図で表せます

https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack


実際に試してみる

登場人物が多いので、やはり身体で覚えるしかありませんね!(脳筋💪)

実際に動かして確認してみることにします

流れ

  1. プロジェクトの作成
  2. Modelファイルの作成
  3. Containerの作成とStoreの読み込み
  4. ViewからEntityの取得と反映
  5. 最終的にできたアプリ

どんなアプリを作る

今回は筆者がひっそり好きな将棋を題材にサンプルを作成していきます

もうちょっとわかりやすい題材にしろよ,,,って?
Core Dataのサンプルなのに補足で将棋の解説を書くことになっている本人が一番わかっています😇

扱うデータ

  • Model
    • Player:将棋棋士に該当します
    • attribute(属性)
      • name: 棋士の名前に該当します
      • title: 対象の棋士が持つ将棋界8大タイトルに該当します

補足:

1. プロジェクトの作成

まずはXcodeから新規プロジェクトを作成するところから流れを掴んでいきましょう

新規プロジェクト作成時にCore Dataを追加することもできますが、どこが関わっているかを明確にするためにもサラの状態にCore Dataを導入してみます

これはお馴染みの流れですね

Xcodeの新規プロジェクト


Appを選択

Use Core Dataにはチェックを入れずに作成する

2. Modelファイルの作成

新規のプロジェクトが生成できたらModelファイルを作成していきます

Core Data >> Data Modelを選択して"Next"を選択します
SampleModelというModel名にします

Add Entityを選択してEntityをModel内に追加します

今回はPlayerという将棋棋士を表すEntityを用いるので、その名称への変更します

Entity(デフォルト)とある部分をダブルクリックでリネーム

次にEntityにAttributeを追加します。Player(将棋棋士)はname(名称)とtitle(保有するタイトル)を持たせたいのでそのように追加します

3. Containerの作成とStoreの読み込み

ここからはようやくコードに触れます🎉

まずは、アプリはContainerを経由してデータベースを扱うので、Containerを作成します

直接Containerを使用するViewなどに定義することもできますが、containerという情報をCore Dataのものとして明確にしたいため、PersistenceControllerを構造体として定義することにします

PersistenceController.swift
import CoreData

struct PersistenceController {
    let container: NSPersistentContainer

    init() {
	// ModelファイルにつけたSampleModelを指定する
        container = NSPersistentContainer(name: "SampleModel")
	// ContainerからStoreを読み込む処理を記述する
        container.loadPersistentStores { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error: \(error)")
            }
        }
    }
}

4. ViewからEntityの取得と反映

Containerからデータを取得してViewに反映させていきます

Viewのどこからでもデータの取得ができるように、今回はEnvironmentオブジェクトとしてContainerを扱います

@main
struct TestCoreDataApp: App {
    let persistenceController = PersistenceController()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

データの取得は@FetchRequestでEntityを指定することで取得できます

struct ContentView: View {
    @Environment(\.managedObjectContext) var viewContext
    @FetchRequest(entity: Player.entity(), sortDescriptors: []) var fetchedPlayerList: FetchedResults<Player>
    
    var body: some View {
	    List(fetchedPlayerList) { player in
            if let name = player.name,
               let data = player.title,
               let title = try? JSONDecoder().decode(Title.self, from: data){
                VStack {
                    Text("棋士名:\(name)")
                    Text("タイトル:\(title.rawValue)")
                }
            }
	    // ・・・
        }
    }
Titleのデコードは何?

Titleは将棋棋士の保有するタイトルを表します
タイトル数が決まっていることを加味してenum定義としています

enum Title: String, CaseIterable, Codable, Identifiable {
    var id: String { rawValue }

    case ryuo = "竜王"
    case meijin = "名人"
    case ouza = "王座"
    case oui = "王位"
    case ousyo = "王将"
    case kisei = "棋聖"
    case kio = "棋王"
    case eio = "叡王"
    case none = "なし"
}

また、データを追加するためにPickerを用いてタイトルを選択させるために、CaseIterableからタイトル名の一覧が取得できるようにしています
https://developer.apple.com/documentation/swiftui/picker

5. 最終的にできたアプリ

追加したい将棋棋士の名前をTextFieldに入力して、Pickerから選択したタイトルを保持させる動作が確認できました

Core Dataとして永続化できているので、アプリをキルしてもデータが残っていることが確認できます

1996年に果たされた羽生善治先生の7大タイトル制覇時を表せていますね

サンプルコードは下記にあります🙋‍♀️
https://github.com/Nao-RandD/TestCoreData/tree/main

Tips:あれ?叡王は?

当時は叡王がなかったため、7大タイトルで全タイトル制覇となります
https://www2.nhk.or.jp/archives/movies/?id=D0009030280_00000


まとめ

筆者のエゴ全開で作りたいサンプルに多少クセがあるため、サンプルとしてはあまり良くなかったかも知れませんね、、、(猛省)

ただ、実際に手を動かしてCore Dataがどのようなものかは掴めた気がしています

触った上で感想は以下のように思いました🤔

👍:メリット

  • GUIからModelファイルを定義でき導入が容易になる
  • UserDefaultsよりもデータを構造として扱いやすくなる

😿:デメリット(少し不便?)

  • 登場人物の把握を含めると多少の学習コストがある
  • Containerの定義部分などはボイラープレートとなる
  • ModelファイルをGUIから行うために作成時の入力ミスが起きやすく、自動化などはしづらい

SwiftDataの内容にも触れて、どういった部分でメリット・デメリットがあるかをより掴んでいこうかと思います


あとがき

ここは間違っているよ、こういった解釈がいいんじゃないか?、など大歓迎です!


参考

Discussion