- 記事一覧 >
- ブログ記事
![category logo](https://res.cloudinary.com/dt8zu6zzd/logos/Swift.png)
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
のみ の単純なエンティティとします。
実装内容の詳細は、下に掲載のソースコード中のコメントを参照してください。1行1行やっていることの説明をコメントに書きました。
追加する処理のところとアラート表示のところの解説コメントは省略しました。
主に、以下の部分がキモになります。
・Task{...}ブロックを使って、非同期関数の deleteRecords()
を呼び出す。
・deleteRecords()
には、async キーワードを付けて、"非同期なコンテキスト"とする。
・fetchRequest.predicate = NSPredicate(format:
で検索条件設定
・newBackgroundContext()
でバックグラウンド用のコンテキストを作成
・NSBatchDeleteRequest(fetchRequest: fetchRequest)
でバッチ削除オブジェクトを生成
・バックグラウンド用のコンテキストで NSBatchDeleteRequest
実行
ソースコード全体像です。実装手順
セクションで説明しますが、新規プロジェクトを作成後、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
をクリックします。
Xcode を起動して、App
を選択して、Next
をクリックします。
Project Name: [任意]
Team: [任意]
Organization Identifier: [任意]
Bundle Identifier: [任意]
Interface: SwiftUI
Language: Swift
Use Core Data: チェック有り
Include Tests: チェック無し
とし、Next
をクリックします。
Use Core Data
にチェックが無くても、Core Data 対応の実装ができますが、作業を単純化するため、付ける前提とします。
ContentView.swift
の内容をそっくりそのまま上記の内容に差し替えます。
Core Data エンティティですが、Use Core Data
にチェックを入れてプロジェクトを作成したため、テンプレート実装が生成されて、最初から、Item
エンティティが存在して、timestamp
属性を持っています。したがって、今回のプログラムでは、何もしなくても良いです。
シュミレーターで起動してみます。(動画は、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/WidgetExampleshttps://github.com/pawello2222/WidgetExamples
Loading and Displaying a Large Data Feedhttps://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed
![comments example](https://res.cloudinary.com/dt8zu6zzd/comments.png)
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。