iOS・AndroidSwiftモバイル

【UIKit × Core Data】CRUD操作を実装する

iOS・Android
この記事は約13分で読めます。

UIKit × Core Dataで、基本的なCRUD操作を行うTODOアプリを実装したので、記録を兼ねた記事を書きました。

成果物

テーブルViewを使用したシンプルなTODOリストを実装しました。

成果物イメージ

以下GitHubリポジトリです。

GitHub - ebibibibibi/CoreDataSumpleApp
Contribute to ebibibibibi/CoreDataSumpleApp development by creating an account on GitHub.

環境

  • Xcode15
  • Swift 5.9
  • iOS 17

Core Data概要

Core DataはAppleの全プラットフォームで使用することのできる、データ永続化(※0)フレームワークです。

アプリケーションの永続的なデータをオフラインで使用できるように保存したり、一時的なデータをキャッシュしたり、アンドゥ機能(※1)の追加をサポートします。さらに、iCloudアカウントを介して複数のデバイス間でデータを同期する機能も提供されています。

永続性:

オブジェクトをデータストアにマッピングする詳細を抽象化する。Core Dataは、UIとデータストアの間で、インターフェースの役割を果たすことができます。複雑なデータベース操作の知識がなくても、アプリケーションのデータを管理することを可能にしています。

アンドゥとリドゥ機能:

アプリケーション内で行われた個々の変更や一括処理された変更を追跡し、それらをロールバックすることができます。

バックグラウンドデータタスク:

バックグラウンドでデータ処理のタスクを実行することができます。
例えば、Webサービスからデータを受取り、それをオブジェクトにパースする作業をバックグラウンドで行い、結果をキャッシュすることができます。

ビューとの同期:

View synchronizationと呼ばれる、アプリケーションのUIと表示されているデータが常に同期されている状態を実現する機能を持っています。

バージョニングとマイグレーション:

Core Dataには、データモデルのバージョン管理や、ユーザのデータ移行を行うメカニズムが含まれています。

※0) 永続化:
プログラムがデータを生成または取得した後、そのデータを永久に保存するプロセス。アプリケーションが終了したりデバイスが再起動されたりしても、データが失われることなく、後から再アクセスできるのは、データが永続化されているから。

※1) アンドゥ機能:
ユーザーが行った操作を取り消し、それ以前の状態に戻す機能を指す。
例えば、ユーザーが間違えた操作をした場合、元の状態に戻すことはアンドゥ機能により実現されます。


事前準備

プロジェクトにData Modelを追加する

最初にTask Enitityを設定します。

AttributeはTitleとisFinishの2つを設定しました。

NSManagedObjectContextを取得する

NSManagedObjectContextはCore Dataの作業領域です。
オブジェクトの作成、変更、保存を行います。

iOS10から導入された新しい機能であり、NSManagedObjectContextを作成することで、NSManagedObjectModel、NSPersistentStoreCoordinator、NSManagedObjectContextの3つの生成と管理を一括で行うことができます。

ここでは、最もベーシックな手法を用いてNSManagedObjectContextを設定しました。

UIApplication.shared.delegateを使用してAppDelegateインスタンスにアクセスし、そこに定義されているpersistentContainerviewContextを取得します。

    private func getContext() ->  NSManagedObjectContext {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        return appDelegate.persistentContainer.viewContext
    }

NSManagedObjectModel、NSPersistentStoreCoordinator、NSManagedObjectContextを逐一生成するパターンの実装がGitHub Gistにありました。内部の挙動に興味のある方は以下参照してください。

これで、最小限Core Dataを操作する準備が整いました。
CRUD操作の実装に着手しましょう。

Read

  1. エンティティのインスタンスを取得するためのフェッチリクエストを作成する。
  2. 必要に応じてフェッチリクエストの結果をソートする。
  3. フェッチリクエストを実行する。

NSManagedObjectContext を取得し、取得するオブジェクトが属するメモリ領域とデータ操作を行うための作業域を準備します。
NSFetchRequest を用いて、 Task エンティティからどのようなデータを取得するか定義を行います。ここでは、 Task オブジェクトを降順に取得するリクエストを作成しています。

NSManagedObjectContextfetchし、取得したデータをtasks配列に格納します。

    let context = getContext()
    let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
    let sortDescription = NSSortDescriptor(key: "title", ascending: false)

    // contextを使用して、fetchRequestに基づいてデータを取得し、tasks配列に格納する
    fetchRequest.sortDescriptors = [sortDescription]
      do {
            tasks = try context.fetch(fetchRequest)
        } catch let error as NSError {
            print(error.localizedDescription)
        }

Create

  1. Core Dataモデル内の操作対象エンティティの定義を取得する。
  2. 定義に基づいて、新規にManagedObjectを作成しコンテキストに挿入する。
  3. ManagedObjectを変更する。
  4. context.save() を呼び出して、変更したManagedObjectを保存する。

NSEntityDescriptionで、Core Dataモデル内の「Task」という名前のエンティティの定義を取得し、指定したコンテキスト(context)内のデータモデルから検索しています。
これにより、Task オブジェクトを作成するために必要なエンティティの構造(属性、リレーションシップなど)を取得しています。

次に、新しいTaskオブジェクトを作成し、データベースのコンテキストに挿入します。
ここでは、init(entity:insertInto:)を用いて、新しいTaskオブジェクトを初期化しています。
タイトルと完了状態を設定したのち、context.save()を呼び出して、新しいTaskオブジェクトの保存を実行します。

save() メソッドは、現在の NSManagedObjectContextに対して行われた変更を永続ストアに変更する責務を持っています。データの変更・削除の時にも使用される、重要な関数です。

    // 新しいタスクを保存する
        internal func saveTask(withTitle title: String) {
        guard let entity = NSEntityDescription.entity(forEntityName: "Task", in: context) else { return      }
        let taskObject = Task(entity: entity, insertInto: context)
        taskObject.title = title
        taskObject.isFinish = false
        do {
            try context.save()
            tasks.append(taskObject)
        } catch let error as NSError {
            print(error.localizedDescription)
        }
    }

関係する公式ドキュメントです。

NSEntityDescription | Apple Developer Documentation
A description of a Core Data entity.
init(entity:insertInto:) | Apple Developer Documentation
Initializes a managed object from an entity description and inserts it into the specified managed object context.
save() | Apple Developer Documentation
Attempts to commit unsaved changes to registered objects to the context’s parent store.

context.save()

context.save()NSError オブジェクトへのポインタをパラメータとして渡されており、エラーが発生した場合に詳細を出力することができます。

hasChanges()

hasChanges() は、コンテキストに未コミットの変更があるかどうかを示すBool値です。
Core Dataが不必要な作業を行う可能性を防ぐため、saveメソッドを呼び出す前に、必ず実行する旨が公式ドキュメントに記述されています。

hasChanges | Apple Developer Documentation
A Boolean value that indicates whether the context has uncommitted changes.

Update

  1. NSFetchRequestを使用して、変更する エンティティをコンテキストから検索する。
  2. 検索で見つかった 操作対象エンティティのプロパティを更新する。
  3. コンテキストに未コミットの変更がある場合、context.save() を呼び出して変更を永続ストアに保存する。

NSFetchRequest を用いて、特定のエンティティ(この記事では Task エンティティ)からどのようなデータを取得するか定義を行います。ここでは、タイトルが同じ Task オブジェクトを取得するリクエストを作成しています。

NSManagedObjectContext を使用して、NSFetchRequest で定義されたリクエストに基づいてデータのフェッチを行います。フェッチされたデータは、必要に応じて更新されます。

     // タスクのステータスを更新する
    internal func changeDone(at indexPath: Int) {
        guard let title = tasks[indexPath].title else {
            print("指定されたタスクにタイトルがない")
            return
        }
        let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
        // フェッチリクエストの結果をフィルタリング
        fetchRequest.predicate = NSPredicate(format: "title == %@", title)
        do {
            let results = try context.fetch(fetchRequest)
            guard let taskToUpdate = results.first else {
                print("タイトルが一致するタスクが見つかりません")
                return
            }
            taskToUpdate.isFinish = !taskToUpdate.isFinish
            
            if context.hasChanges {
                try context.save()
            }
        } catch let error as NSError {
            print(error.localizedDescription)
        }
    }
    
predicate | Apple Developer Documentation
The predicate of the fetch request.
NSFetchRequest | Apple Developer Documentation
A description of search criteria used to retrieve data from a persistent store.
Introduction
Describes how to specify queries in Cocoa.
fetch(_:) | Apple Developer Documentation
Returns an array of objects that meet the criteria of the specified fetch request.

Delete

  1. 削除するエンティティをコンテキストから検索する。
  2. エンティティの削除を行う。
  3. 変更内容を保存する。

delete()メソッドを通じ、指定されたオブジェクトは、変更がコミットされるときにその永続ストアから削除されるべきだと明示されます。

コンテキストに対してsave()メソッドを呼び出し、削除を永続ストアへ反映します。

    // タスクの削除を行う
    internal func deleteTask(at indexPath: Int) {
        guard let title = tasks[indexPath].title else {
            print("指定されたタスクにタイトルがない")
            return
        }
        let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
        // フェッチリクエストの結果をフィルタリング
        fetchRequest.predicate = NSPredicate(format: "title == %@", title)
        do {
            let results = try context.fetch(fetchRequest)
            guard let taskToDelete = results.first else {
                print("タイトルが一致するタスクが見つかりません")
                return
            }
            context.delete(taskToDelete)
            // コンテキストに変更がある場合のみ保存を試みる
            if context.hasChanges {
                try context.save() // 変更を保存
                tasks.remove(at: indexPath) // 配列からもタスクを削除
            }
        } catch let error as NSError {
            print(error.localizedDescription)
        }
    }
delete(_:) | Apple Developer Documentation
Specifies an object that should be removed from its persistent store when changes are committed.

まとめ

Core Dataの概要と、主要な機能。CRUD操作の具体的な実装方法を説明しました。
本稿がCore Dataを理解する上で一助となれば幸いです。ここまで読んでくださりありがとうございました。