1. 記事一覧 >
  2. ブログ記事
category logo

SwiftUI Core Data バックグラウンドでレコードを削除

(更新) (公開)

はじめに

Swift(SwiftUI) で バックグラウンドで、Core Data のレコードを削除するアプリを作成しました。
アプリと言っても、ダミーデータ追加、削除、Core Data 内データ表示の最低限のものです。
今回、実装内容と速攻で実装して動作確認する手順を書きます。


【検証環境】

macOS Monterey 12.6

 Xcode 13.4.1

 Swift 5.7 (SwiftUI)

Core Data とは、とか、Core Data にデータを入れる方法についての説明は省略します。バックグラウンドでデータ削除部分と実装手順に焦点を当てます。

本記事の内容により何らかの不都合が生じても当方は一切責任を負いません。


動作内容

動作内容は、以下です。


データ表示ボタンタップ
Core Data の全内容をアラートで表示しています。
最初は、何もないから、空表示です。

追加ボタンタップ
現在~一年前までのランダムなタイムスタンプを 10 件 Core Data に挿入します。
同時に Core Data の全レコードをアラートで表示しています。
10 件データがあることが分かります。

削除ボタンタップ
半年前のタイムスタンプを Core Data から削除しています。

データ表示ボタンタップ
Core Data の全レコードをアラートで表示しています。
半年前のタイムスタンプのデータが消えていることが分かります。


実装内容

今回扱う Core Data エンティティ(Entity、RDB でいうテーブル)は、属性(Attribute、RDB でいうカラム)に timestamp のみ の単純なエンティティとします。

Core Dataエンティティ


実装内容の詳細は、下に掲載のソースコード中のコメントを参照してください。1行1行やっていることの説明をコメントに書きました。

追加する処理のところとアラート表示のところの解説コメントは省略しました。


主に、以下の部分がキモになります。
Task{...}ブロックを使って、非同期関数の deleteRecords() を呼び出す。
deleteRecords() には、async キーワードを付けて、"非同期なコンテキスト"とする。
fetchRequest.predicate = NSPredicate(format: で検索条件設定
newBackgroundContext()でバックグラウンド用のコンテキストを作成
NSBatchDeleteRequest(fetchRequest: fetchRequest)でバッチ削除オブジェクトを生成
バックグラウンド用のコンテキストで NSBatchDeleteRequest 実行

Taskブロック ソースコード


deleteRecords ソースコード


だいたいのイメージ

実装内容のイメージ図


ソースコード全体像です。
実装手順 セクションで説明しますが、新規プロジェクトを作成後、ContentView.swift しか変更しません。

ContentView.swift
import SwiftUI
import CoreData
import UIKit

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    var body: some View {
        VStack{
            Button(action: {
                showRecords()
            }){
                Text("データ表示").font(.title)
            }
            Button(action: {
                addRecords()
                showRecords()
            }){
                Text("追加").font(.title)
            }
            Button(action: {
                Task {
                    // Task{...}ブロックを使って、非同期関数の deleteRecords() を呼び出す。
                    do {
                        // Taskの中は非同期的なコンテキスト。非同期関数を呼び出せる。
                        try await deleteRecords()
                    } catch {
                        let nsError = error as NSError
                        // ErrorからNSErrorにキャスト
                        // エラーをNSErrorにキャストして欲しい情報を参照
                        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                        // 【NSErrorの情報】
                        // domain: エラーの種類を識別するための文字列
                        // code: エラーの種類を識別するための整数値
                        // userInfo: エラーに関する付加情報
                    }
                }
            }){
                Text("削除").font(.title)
            }
        }
    }
    private func showRecords() {
        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Item")
        let sortDescripter = NSSortDescriptor(key: "timestamp", ascending: false)
        fetchRequest.sortDescriptors = [sortDescripter]
        var line:String = ""
        do {
            let myResults = try viewContext.fetch(fetchRequest)
            for myData in myResults {
                var dateData = ""
                let formatter = DateFormatter()
                formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
                dateData = formatter.string( from: myData.value(forKey: "timestamp") as! Date )
                line = line + dateData + "\n"
            }
        } catch {
        }
        let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        let rootVC = windowScene?.windows.first?.rootViewController
        let dialog = UIAlertController(title: "データ一覧", message: line, preferredStyle: .alert)
        dialog.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        rootVC?.present(dialog, animated: true, completion: nil)
    }

    private func addRecords() {
        withAnimation {
            for _ in 1...10 {
                let newItem = Item(context: viewContext)
                var goBackTime = 0
                goBackTime = goBackTime + (Int.random(in:0..<365) * 24 * 60 * 60)
                goBackTime = goBackTime + (Int.random(in:0..<24) * 60 * 60)
                goBackTime = goBackTime + (Int.random(in:0..<60) * 60)
                goBackTime = goBackTime + Int.random(in:0..<60)
                newItem.timestamp = Calendar.current.date(byAdding: .second, value: ( goBackTime * -1 ) , to: Date())!
                do {
                    try viewContext.save()
                } catch {
                    let nsError = error as NSError
                    fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                }
            }
        }
    }
}

private func deleteRecords() async throws {
    // 並行処理機能 "Swift Concurrency"
    // Swift ConcurrencyはSwift 5.5から登場した並行処理を言語機能レベルでサポートする機能。
    // async キーワードにより、この関数の中は "非同期なコンテキスト" ("asynchronous context")になる。
    enum DeleteError: Error {
        case batchDeleteError
        var errorDescription: String? {
            switch self {
            case .batchDeleteError:
                return NSLocalizedString("Failed to execute a batch delete request.", comment: "")
                // エラー文言をローカライズ(言語環境に合わせて翻訳)
                // 関数func NSLocalizedString(_ key: String, comment: String) -> String
                // は、リソースにあるLocalizable.stringsファイルを参照し、key値を検索して、その値の文字列を返り値とする。
                // keyが見つからない時は、key。value: を指定すれば、keyではなくて指定した文字列を出力する。
                // comment: 引数に与えた文字は単なるコメント。出力されない。
            }
        }
    }

    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
    // エンティティ(RDBでいうテーブル)Itemに対しての検索リクエストを設定(NSFetchRequest)
    //半年前
    fetchRequest.predicate = NSPredicate(format: "timestamp < %@", NSDate(timeIntervalSinceNow: -24 * 60 * 60 * 183 ))
    //すべて(現在から0秒前のタイムスタンプ)
    // fetchRequest.predicate = NSPredicate(format: "timestamp < %@", NSDate(timeIntervalSinceNow: 0 ))
    // fetchRequest.predicateは、抽出条件の指定
    // nil(抽出条件無し=全て)がデフォルト値
    // 抽出条件は、NSPredicateクラスを使用して指定
    // format: のところに様々な条件が書ける。(SQLで言うwhereのようなもの)
    // Core Dataのtimestamp属性(RDBでいうカラム)を条件に指定して、○秒前より古いデータを抽出
    // %@ は、2番目の引数 NSDate(timeIntervalSinceNow: -24 * 60 * 60 * 183 )の値が当てはまる(直接書かない)
    // timeIntervalSinceNow は、○秒前
    let context = PersistenceController.shared.container.newBackgroundContext()
    // 【PersistenceController.shared】
    // Core Dataに必要なオブジェクトの作成と管理を行う永続コンテナ(PersistentContainer)のコントローラーを生成
    // 【.container】
    // NSPersistentContainer(iOS10から追加)
    // NSPersistentContainerは、Core Dataを扱うための機能が全部入ったクラス。Core Data stack。
    // 【.newBackgroundContext()】
    // NSPersistentContainerのインスタンスメソッドであるnewBackgroundContextを実行
    // newBackgroundContextは、バックグラウンド用のコンテキストを作成するためのメソッド
    // newBackgroundContextを呼び出したpersistentContainerが持つpersistentStoreCoordinatorが自動でセットされる
    // 【persistentStoreCoordinator】
    // 永続ストア(実際に保存される側)と管理オブジェクトコンテキスト(メモリ)間を仲介する
    try await context.perform {
    // 【perform】
    // NSManagedObjectContextのインスタンスメソッドであるperformを使用して、
    // コンテキスト内部にシリアルキューに登録
    // 引数で渡したクロージャーが非同期に呼び出され、サブスレッドで実行される
        let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        // 【NSBatchDeleteRequest】
        // 削除をバッチで行うためのクラス。iOS9~実装されている。
        // オブジェクトをメモリにロードせずに、SQLite 永続ストア内のオブジェクトを削除。
        // fetchRequest: 上で生成している検索リクエストを設定したもの(NSFetchRequest)
        guard let fetchResult = try? context.execute(batchDeleteRequest),
            // try? : 例外が発生しなければその関数の戻り値を返し、例外が発生すればnilを返す。,は、&&と同義(カンマの前が成功したら、後を実行)
            // バックグラウンド用のコンテキストでSQLでいう"delete from ... where ..."実行
            // executeメソッド→func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult
            let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
            // NSPersistentStoreResultからNSBatchDeleteResultにキャスト
            let success = batchDeleteResult.result as? Bool, success
            // 最終的にブール値を返す。(使っていない。)
        else {
            throw DeleteError.batchDeleteError
            // 上の enum DeleteError: Error { のところで定義しているエラーをthrow
        }
    }
}

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

実装手順

Xcode を起動して、Create a new Xcode project をクリックします。 Create a new Xcode project クリック


Xcode を起動して、App を選択して、Next をクリックします。 App 選択 Next クリック


Project Name: [任意]
Team: [任意]
Organization Identifier: [任意]
Bundle Identifier: [任意]
Interface: SwiftUI
Language: Swift
Use Core Data: チェック有り
Include Tests: チェック無し
とし、Next をクリックします。

Use Core Data チェック Next クリック

Use Core Data にチェックが無くても、Core Data 対応の実装ができますが、作業を単純化するため、付ける前提とします。


保存する場所を選択して、Create をクリックします。 保存する場所を選択して、Create クリック


ContentView.swift の内容をそっくりそのまま上記の内容に差し替えます。 ContentView.swiftを上記の内容に差し替え


Core Data エンティティですが、Use Core Data にチェックを入れてプロジェクトを作成したため、テンプレート実装が生成されて、最初から、Item エンティティが存在して、timestamp 属性を持っています。したがって、今回のプログラムでは、何もしなくても良いです。 Core Dataエンティティ


シュミレーターで起動してみます。(動画は、iPhone12のシュミレーターです。)


ヨシ!


参考サイト

【SwiftUI】Core Data の使い方:準備編
https://capibara1969.com/3209/


【SwiftUI】Core Data の使い方:標準テンプレートを読み解く
https://capibara1969.com/3178/


【Swift】Core Data をバックグラウンドで使う
https://www.2nd-walker.com/2020/10/09/swift-using-coredata-in-background/


【Swift】Core Data の基本的な使い方
https://www.2nd-walker.com/2020/03/05/swift-basic-how-to-use-coredata/


pawello2222/WidgetExamples
https://github.com/pawello2222/WidgetExamples


Loading and Displaying a Large Data Feed
https://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed

loading...