はじめに
こんにちは!システム開発部のYです。
WWDC2023で、SwiftDataが発表されました。iOS17から利用することができる、アプリ内DBです。
https://developer.apple.com/xcode/swiftdata/
アプリ内DBは他にもあり、CoreDataや外部ライブラリのRealmなどがあります。SwiftDataは、SwiftUIのように使いやすくしたCoreDataとされ、実質的なCoreDataの後継となります。そもそも筆者はCoreDataも使用したことがないので、この際CoreDataの学習の意味も兼ねて、CoreDataについての記事も書くことにしました。今回はCoreData編とします。
環境
- Swift Version: 5.9
- Xcode Version: 15
- iOS: 17.0
- macOS: 13.5
目次
CoreDataとは
モデルオブジェクトを永続化できるフレームです。
オブジェクトとRDB(リレーショナルデータベース)の橋渡し役を担っています。
CoreDataは、iOS3からあり長年使用されてきたフレームワークです。https://developer.apple.com/documentation/coredata/
CoreDataの使い方、サンプルの実装
簡単なTODOアプリを実装します。
Titleと説明を入力するとCoreDataに保存し保存されたデータを取得、リストに表示します。
そしてデータはアプリを閉じた後も表示されます。また、検索バーで値の絞り込み機能も実装します。
CoreData使用準備
データモデルの作成
プロジェクトは作成済とします。
xcdatamodeldの作成を行います。「File → New → File…」から Data Modelの作成を行います。
名前はSampleModelとします。
データモデルの設計
作成したデータモデルにEntityを追加します。
SampleModelを開き、左下のAdd Entityを選択します。
Entitiesが作成されるので名前を今回は「ToDo」という名前にします。
ToDoアプリということなので、今回は以下の5つのAttributeを追加します。
・id: 識別するための一意になる値(UUID)
・title: タイトル(String)
・desc: 説明(String)
・createdTime: 作成時間(Date)
・checked: 完了済みかどうか(Bool)
NSManagedObjectの作成
EntitiesのCodegenを「Manual/None」に変更します。
※デフォルトは、「Class Definision」です。Class Definisionの場合自動的にエンティティの派生クラスが作成されます。カスタマイズをしなければこのままで問題ありません。
Editor → Create NSManagedObject Subclass… を選択して、NSManagedObjectファイルの作成を行います。
1つのDataModelに対して、2つのファイルが作成されます。
NSPersistentContainerの作成と、Storeの読み込む
使用する場所で直接書く事はできますが、今回はPersistenceControllerとして切り出したいと思います。
CoreDataを使用するので、もちろんimport CoreDataをする必要があります。
そして、NSPersistentContainerを宣言しinitでコンテナの初期化、ストアの読み込みと設定を行います。
PersistenceController.swift
import CoreData
struct PersistenceController {
/// Core Dataの永続データストアにアクセスするためのコンテナ
let container: NSPersistentContainer
init() {
// Core Dataコンテナの初期化
container = NSPersistentContainer(name: "SampleModel")
// 永続ストアの読み込みと設定
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
NSPersistentContainerをViewで使いまわせる様にする
(プロジェクト名)App.swiftファイルに追加します。
ContentViewで使用するので、使いまわせるように@Environment(\.managedObjectContext)にNSManagedObjectContextを登録する処理を記載します。
SampleCoreDataApp.swift
import SwiftUI
@main
struct SampleCoreDataApp: App {
let persistenceController = PersistenceController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
データの取得、保存、削除
データの取得(ソート無し)
SwiftUIでは、大変便利なプロパティラッパー(@FetchRequest)が用意されています。
@FetchRequestでデータ取得を行います。プロパティに検索結果が格納され、データの変更に応じて検索結果が常に更新されます。
sortDescriptors(検索結果のソート順)に何もしていない為、ソートはされません。
@FetchRequest(
entity: ToDo.entity(),
sortDescriptors: [],
predicate: nil,
animation: .default)
var toDoList: FetchedResults<ToDo>
FetchRequestについては以下を参照
https://developer.apple.com/documentation/swiftui/fetchrequest
データの取得(ソートあり)
sortDescriptors(検索結果のソート順)を指定します。以下は、createdTimeの昇順で並び替えを行なっています。
@FetchRequest(
entity: ToDo.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \ToDo.createdTime, ascending: true)],
predicate: nil,
animation: .default)
var toDoList: FetchedResults<ToDo>
またソート順の指定を重ねることもできます。
データの取得(抽出条件の指定)
抽出条件は、predicateにNSPredicateを指定します。以下は、titleがiOSのみのデータを取得しています。
@FetchRequest(
entity: ToDo.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "title == 'iOS'"),
animation: .default)
var toDoList: FetchedResults<ToDo>
以下は、titleにiOSを含んでいるデータのみ取得しています。
@FetchRequest(
entity: ToDo.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "title CONTAINS %@", "iOS"),
animation: .default)
var toDoList: FetchedResults<ToDo>
抽出条件のバリエーションは以下のサイトが参考になりました。
https://hajihaji-lemon.com/swift/coredata-nspredicate/
また抽出条件の指定も重ねることができます。
データの保存
NSManagedObjectの派生クラスのインスタンスを生成をし、NSManagedObjectContextに変更を保存することで実現できます。
保存するには、NSManagedObjectContextのsaveメソッドを使用します。
@Environment(\.managedObjectContext) var viewContext
private func addItem() {
// インスタンスを生成
let addToDo = ToDo(context: viewContext)
addToDo.id = UUID()
addToDo.title = inputToDoModel.title
addToDo.desc = inputToDoModel.desc
addToDo.createdTime = Date()
// データベースファイルに保存
try? viewContext.save()
}
データの削除
NSManagedObjectContextのdeleteメソッドを使用します。
引数に削除するエンティティのインスタンスを指定します。
deleteメソッドを呼んだ後に、データの保存時と同じようにsaveメソッドを呼ぶ必要があります。
private func deleteItems(offsets: IndexSet) {
offsets.map { toDoList[$0] }.forEach(viewContext.delete)
try? viewContext.save()
}
Previewについて
SwiftUIのPreview機能を使用する場合、現状のままだとCrashします。なので、Preview用の初期データを用意します。PersistenceControllerとして切り出したファイルに、Preview用のPersistenceControllerを追加します。
extension PersistenceController {
/// Preview用
static var previewContainer: PersistenceController = {
let result = PersistenceController()
let viewContext = result.container.viewContext
for i in 0..<10 {
let toDoItem = ToDo(context: viewContext)
toDoItem.id = UUID()
toDoItem.title = "title: \(i)"
toDoItem.desc = "desc: \(i)"
toDoItem.createdTime = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
}
そして、Previewで上記のpreviewContainerを呼び出します。
#Preview {
ContentView()
.environment(\.managedObjectContext, PersistenceController.previewContainer.container.viewContext)
}
動的な抽出条件の実装(検索バー)
nsPredicateを変更すれば実現できます。
① 入力内容を監視するための変数を用意。
@State private var filterText = ""
② 検索バーの実装、NavigationViewにsearchable(text:placement:)モディファイアを実装
※ iOS15以上から、下記参照
https://developer.apple.com/documentation/swiftui/view/searchable(text:placement:prompt:)-18a8f
NavigationView {
...
}
.searchable(text: $filterText)
③ NavigationViewにonChangeモディファイアを実装し、変更を監視
.onChange(of: filterText) { oldValue, newValue in
filterToDoList(text: newValue)
}
④ 入力されたテキストを元に、NSPredicateを変更します。
private func filterToDoList(text: String) {
if text.isEmpty {
toDoList.nsPredicate = nil
} else {
toDoList.nsPredicate = NSPredicate(format: "title CONTAINS %@", text)
}
}
最終的なコード
import SwiftUI
class InputToDoModel: ObservableObject {
@Published var title = ""
@Published var desc = ""
}
struct ContentView: View {
@Environment(\.managedObjectContext) var viewContext
@FetchRequest(
entity: ToDo.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \ToDo.createdTime, ascending: true)],
predicate: nil,
animation: .default)
var toDoList: FetchedResults<ToDo>
@State private var isButtonPressed = false
@State private var filterText = ""
@ObservedObject private var inputToDoModel: InputToDoModel = .init()
var body: some View {
NavigationView {
List {
ForEach(toDoList) { toDo in
if let title = toDo.title,
let desc = toDo.desc,
let createdAtDate = toDo.createdTime {
HStack {
Image(systemName: toDo.checked ? "checkmark.circle.fill" : "circle")
VStack(alignment: .leading) {
Text(title)
Text(desc)
HStack {
Text("作成時間:")
Text(createdAtDate, formatter: itemFormatter)
}
}
}
// contentShapeでタップ領域を広げる
.contentShape(Rectangle())
.onTapGesture {
toDo.checked.toggle()
try? viewContext.save()
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button {
isButtonPressed.toggle()
} label: {
Label("Add Item", systemImage: "plus")
}
}
}
}
.alert("入力してください。", isPresented: $isButtonPressed) {
TextField("input your Title", text: $inputToDoModel.title)
TextField("input your Description", text: $inputToDoModel.desc)
Button("OK", role: .cancel){
addItem()
}
Button("Cancel", role: .destructive){
// Nop
}
}
.searchable(text: $filterText)
.onChange(of: filterText) { oldValue, newValue in
filterToDoList(text: newValue)
}
}
private func addItem() {
let addToDo = ToDo(context: viewContext)
addToDo.id = UUID()
addToDo.title = inputToDoModel.title
addToDo.desc = inputToDoModel.desc
addToDo.createdTime = Date()
try? viewContext.save()
inputToDoModel.title = ""
inputToDoModel.desc = ""
}
private func deleteItems(offsets: IndexSet) {
offsets.map { toDoList[$0] }.forEach(viewContext.delete)
try? viewContext.save()
}
private func filterToDoList(text: String) {
if text.isEmpty {
toDoList.nsPredicate = nil
} else {
toDoList.nsPredicate = NSPredicate(format: "title CONTAINS %@", text)
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
}
#Preview {
ContentView()
.environment(\.managedObjectContext, PersistenceController.previewContainer.container.viewContext)
}
まとめ
CoreDataの使い方などについてまとめました。データモデルはGUIで定義をするので、テーブル定義をGUIで行いたい方はいいと感じました。また、Core Dataはデータの永続性を自動的に処理してくれるので中身の処理を意識せずに開発できます。
しかし、データモデルの設計やCore Dataの設定について知らないといけないため、学習コストが高いと感じました。
この記事がお役に立てれば幸いです。また、間違っている点などありましたら、教えていただけると幸いです。
次の記事では、SwiftDataについて書こうと思います。








