SwiftUIのアプリ開発で、CoreDataを利用してデータを保存する方法を、サンプルアプリのコードと一緒にまとめます。
CoreDataとは
iPhoneアプリの開発で、メモアプリやTodoアプリなど、ユーザーが入力したり選択したデータは、そのままでは保存されずにアプリを閉じるとリセットされてしまいます。
そのためアプリを閉じてもデータを残しておきたい時には、データを永続化するための処理を組み込むことが必要です。
アプリの設定など一度設定したら増えないようなデータであれば、UserDefaultというメモリに保存することで事足りるのですが、UserDefaultは使用すればするほど増えていくようなデータの保存には向きません。
そこで使えるのが、CoreDataです。
iCloudとの連携もかんたんですし、個々のユーザーのiOS端末の容量を利用するので、アプリ開発者がサーバーなどの保存場所を用意する必要もなし。とってもお手軽。
保存されるデータはSQLのようなデータベース形式で保存されていってくれるので、使い勝手も良い感じ。
CoreDataについてもっと詳しく知りたい方はAppleの公式ドキュメントをご確認ください。
CoreDataを使用してデータ作成・編集・保存・カード型で一覧表示するアプリのサンプルコード
ここからは早速コードと共にCoreDataの使い方を見てみましょう。
サンプルアプリは、
- テキスト
- 日付(選択式)
- お気に入りマーク
というデータを入力画面(シート)で入力して保存、カード形式で表示、カードの長押しから編集・削除できるアプリです。
(カメラロール写真の取り扱いについては別記事にまとめるのですが、データの保存先と処理だけ今回一緒に用意してしまってます。)
開発環境 | バージョン |
---|---|
Xcode | 12.5.1 |
iOS | 14.0以降 |
macOS | BigSur 11.5.2 |
CoreDataのデータベースを設定する
プロジェクト開始時にCoreDataにチェックを入れると、自動でファイルとコードが追加された状態で開始されます。
早速、データベースのテーブル的なやつ(Entity)を設定しましょう。
- 「.xcdatamodeld」ファイルを選択
- 「AddEntity」をクリック
- Entity名を編集(データテーブル名)
最初からあった「item」Entity名をクリックし「delete」キーで削除 - Attributeに保存したいデータ名とType(データ型)を追加する
ここで追加したAttributeが、SQLでいうところのカラムになります。 - 画像データのtypeは「BinaryData」とし、「AllowsExternalStorage」にチェックを入れる
作ったCoreDataのEntityを使えるようにする
- Entity名をクリック
- アイコンをクリック
- 「Codegen」は「Manual/None」を選択
- 上のツールバー「Editor」メニューの「Create NSManagedObject Subclass」をクリック
- 作成するEntityを選択し、アプリのフォルダ内に「create」する
- 「Entity名+CoreDataProperties.swift」
「Entity名+CoreDataClass.swift」
「Persistence.swift」
の3つのファイルが作成されていることを確認
CoreData用のファイルを編集
CoreData用のファイルが自動作成されました。
このままだとちょっと使えないので、コードを編集しましょう。
Persistence.swift
一部削除して一部追加します。
//
// Persistence.swift
// SampleApp
//
// Created by yaguchisato on 2021/11/26.
//
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
//ここは削除
// for _ in 0..<10 {
// let newItem = Item(context: viewContext)
// newItem.timestamp = Date() }
//ここを追加「SampleData」はEntity名に直してね
let newSampleData = SampleData(context: viewContext)
newSampleData.id = UUID()
newSampleData.date = Date()
newSampleData.text = ""
newSampleData.bool = false
newSampleData.image1 = Data.init()
newSampleData.image2 = Data.init()
//以下ひとまず変更なし,cloudkitについては別記事.
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "SampleApp")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Entity名+CoreDataProperties.swift
CoreDataのデータ型はオプショナルになるので、nilの場合の処理を追加しています。
その時々で設定する場合は元のまま何も追加しなくていいです。
//
// SampleData+CoreDataProperties.swift
// SampleApp
//
// Created by yaguchisato on 2021/11/29.
//
//
import Foundation
import CoreData
extension SampleData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<SampleData> {
return NSFetchRequest<SampleData>(entityName: "SampleData")
}
@NSManaged public var bool: Bool
@NSManaged public var date: Date?
@NSManaged public var id: UUID?
@NSManaged public var image1: Data?
@NSManaged public var image2: Data?
@NSManaged public var text: String?
}
//毎回nilの場合の処理を考えるのが面倒なのでまとめて設定
extension SampleData{
public var wrappedDate: Date {date ?? Date()}
public var wrappedId: UUID {id ?? UUID()}
public var wrappedImg1: Data {image1 ?? Data.init(capacity: 0)}
public var wrappedImg2: Data {image2 ?? Data.init(capacity: 0)}
public var wrappedText: String {text ?? ""}
}
extension SampleData : Identifiable {
}
Entity名+CoreDataClass.swift
そのままでOK。
新たに作成するSwiftUIViewファイル/Modelファイル
CoreDataを使う準備ができたので、見た目の部分と動的にデータを作成・編集・保存するためのファイルを作ります。
全部一つのファイルにコードを書くとごちゃごちゃしてしまうので、いくつかファイル分けします。
フォルダ分けしてこんな感じのファイル構造になってます。
ファイルとフォルダはXcode左下の「+」から作成できますよ。
それぞれのファイルのコードを載せますね。
SampleModel.swift データの保存と編集処理
まずは入力されたデータの保存と、既存データがある場合は入力画面に表示して編集できるように、モデルを作成します。
//
// SampleModel.swift
// SampleApp
//
// Created by yaguchisato on 2021/11/29.
// CoreDataへの保存・編集の処理
import Foundation
import SwiftUI
import CoreData
class SampleModel : ObservableObject{
@Published var date = Date()
@Published var id = UUID()
@Published var bool = false
@Published var image1: Data = Data.init()
@Published var image2: Data = Data.init()
@Published var text = ""
@Published var isNewData = false
@Published var updateItem : SampleData!
func writeData(context :NSManagedObjectContext){
//データが新規か編集かで処理を分ける
if updateItem != nil {
updateItem.date = date
updateItem.bool = bool
updateItem.image1 = image1
updateItem.image2 = image2
updateItem.text = text
try! context.save()
updateItem = nil
isNewData.toggle()
date = Date()
bool = false
image1 = Data.init()
image2 = Data.init()
text = ""
return
}
//データ新規作成
let newSampleData = SampleData(context: context)
newSampleData.date = date
newSampleData.id = UUID()
newSampleData.bool = bool
newSampleData.image1 = image1
newSampleData.image2 = image2
newSampleData.text = text
do{
try context.save()
isNewData.toggle()
date = Date()
bool = false
image1 = Data.init()
image2 = Data.init()
text = ""
}
catch {
print(error.localizedDescription)
}
}
//編集の時は既存データを利用する
func editItem(item: SampleData){
updateItem = item
date = item.wrappedDate
id = item.wrappedId
bool = item.bool
image1 = item.wrappedImg1
image2 = item.wrappedImg2
text = item.wrappedText
isNewData.toggle()
}
}
SheetView.swift データ入力画面
データを入力する画面のViewです。
//
// SheetView.swift
// SampleApp
//
// Created by yaguchisato on 2021/11/29.
// データ入力ページのView
import SwiftUI
struct SheetView: View {
@ObservedObject var sampleModel : SampleModel
@Environment(\.managedObjectContext)private var context
var body: some View {
VStack(alignment: .leading){
//キャンセルボタンと保存ボタン
HStack {
//キャンセルの場合は保存せず閉じる
Button("Cansel", action:{
sampleModel.isNewData = false
}).foregroundColor(.blue)
Spacer()
//保存の場合はモデルでデータ処理
Button("Save", action:
{
sampleModel.writeData(context: context)
}
).foregroundColor(.blue)
}
.padding(.bottom, 20.0)
//日付の選択
DatePicker("", selection: $sampleModel.date, displayedComponents: .date)
.labelsHidden()
//文字の入力
TextField("入力できます", text: $sampleModel.text).textFieldStyle(RoundedBorderTextFieldStyle())
//Bool値の選択ボタン
Button(action: {sampleModel.bool.toggle()}){
Image(systemName: sampleModel.bool ? "star.fill":"star")
}
Spacer()
}.padding()
}
}
//プレビュー用コード
struct SheetView_Previews: PreviewProvider {
static var previews: some View {
SheetView(sampleModel: SampleModel()).environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
SampleCardView.swift 保存されたデータをカード型で表示するための雛形
ContentViewで使う、CoreData表示用カードの雛形を作ります。
//
// SampleCardView.swift
// SampleApp
//
// Created by yaguchisato on 2021/11/29.
// データ表示用のカードの雛形View
import SwiftUI
struct SampleCardView: View {
@Environment(\.managedObjectContext) private var context
@ObservedObject var sampleModel : SampleModel
@ObservedObject var samples : SampleData
var body: some View {
HStack{
//CoreDataに保存されたデータを表示
VStack {
Text(samples.wrappedDate, formatter: itemFormatter)
Text(samples.wrappedText)
}
Image(systemName: samples.bool ? "star.fill":"star")
}
//カードの形
.frame(
minWidth:UIScreen.main.bounds.size.width * 0.9,
maxWidth: UIScreen.main.bounds.size.width * 0.9,
minHeight:UIScreen.main.bounds.size.height * 0.2,
maxHeight: UIScreen.main.bounds.size.height * 0.8
)
.background(RoundedRectangle(cornerRadius: 15)
.fill(Color(.gray)))
//カード長押しで編集と削除のボタンを表示
.contextMenu{
Button(action: {
sampleModel.editItem(item: samples)}){
Image(systemName: "pencil")
.foregroundColor(Color.blue)
}
Button(action: {
context.delete(samples)
try! context.save()}){
Image(systemName: "trash")
.foregroundColor(Color.blue)
}
}
}
//日付表示のフォーマット
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "en_US")
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
}
ContentView.swift トップ画面
保存されたデータをカードの雛形に載せて順番に表示し、入力用シートを開くボタンを設置。
//
// ContentView.swift
// SampleApp
//
// Created by yaguchisato on 2021/11/26.
// メインView
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@StateObject private var sampleModel = SampleModel()
@FetchRequest(
// sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
// animation: .default)
// private var items: FetchedResults<Item>
//データの取得方法を指定 下記は日付降順
entity:SampleData.entity(),sortDescriptors: [NSSortDescriptor(keyPath: \SampleData.date, ascending: false)],
animation: .default)
private var samples: FetchedResults<SampleData>
var body: some View {
VStack{
//新規作成ボタン
Button(action:
{sampleModel.isNewData.toggle()
}){
Text("新規作成はここをクリック")
}
//タップするとシートが開く
.sheet(isPresented: $sampleModel.isNewData,
content: {
SheetView(sampleModel: sampleModel)
})
//データを表示する
//スクロール表示については別記事でまた紹介します
List{
ForEach(samples){samples in
SampleCardView(sampleModel: sampleModel, samples: samples)
}
}
}
}
}
//プレビュー用コード
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
以上です。意外とかんたんではないかと思います。
Next
こんな感じで、CoreDataを使用したアプリができました。
次はこのアプリにプラスする形で、PHPickerViewControllerで複数枚の写真をカメラロールから追加する方法をまとめます。
つづく!