スポンサーリンク

[SwiftUI]CoreDataを使用してデータ作成・編集・保存・カード型で一覧表示する

coredatatop
スポンサーリンク

SwiftUIのアプリ開発で、CoreDataを利用してデータを保存する方法を、サンプルアプリのコードと一緒にまとめます。

スポンサーリンク

CoreDataとは

iPhoneアプリの開発で、メモアプリやTodoアプリなど、ユーザーが入力したり選択したデータは、そのままでは保存されずにアプリを閉じるとリセットされてしまいます。

そのためアプリを閉じてもデータを残しておきたい時には、データを永続化するための処理を組み込むことが必要です。

アプリの設定など一度設定したら増えないようなデータであれば、UserDefaultというメモリに保存することで事足りるのですが、UserDefaultは使用すればするほど増えていくようなデータの保存には向きません。

そこで使えるのが、CoreDataです。

iCloudとの連携もかんたんですし、個々のユーザーのiOS端末の容量を利用するので、アプリ開発者がサーバーなどの保存場所を用意する必要もなし。とってもお手軽。

保存されるデータはSQLのようなデータベース形式で保存されていってくれるので、使い勝手も良い感じ。

CoreDataについてもっと詳しく知りたい方はAppleの公式ドキュメントをご確認ください。

CoreDataを使用してデータ作成・編集・保存・カード型で一覧表示するアプリのサンプルコード

ここからは早速コードと共にCoreDataの使い方を見てみましょう。
サンプルアプリは、

  • テキスト
  • 日付(選択式)
  • お気に入りマーク

というデータを入力画面(シート)で入力して保存、カード形式で表示、カードの長押しから編集・削除できるアプリです。
(カメラロール写真の取り扱いについては別記事にまとめるのですが、データの保存先と処理だけ今回一緒に用意してしまってます。)

編集で開いたシートでは既存のデータが表示されて変更できます。
開発環境バージョン
Xcode12.5.1
iOS14.0以降
macOSBigSur 11.5.2

CoreDataのデータベースを設定する

プロジェクト開始時にCoreDataにチェックを入れると、自動でファイルとコードが追加された状態で開始されます。

早速、データベースのテーブル的なやつ(Entity)を設定しましょう。

  1. 「.xcdatamodeld」ファイルを選択
  2. 「AddEntity」をクリック
  3. Entity名を編集(データテーブル名)
    最初からあった「item」Entity名をクリックし「delete」キーで削除
  4. Attributeに保存したいデータ名とType(データ型)を追加する
    ここで追加したAttributeが、SQLでいうところのカラムになります。
  5. 画像データのtypeは「BinaryData」とし、「AllowsExternalStorage」にチェックを入れる

作ったCoreDataのEntityを使えるようにする

  1. Entity名をクリック
  2. アイコンをクリック
  3. 「Codegen」は「Manual/None」を選択
  1. 上のツールバー「Editor」メニューの「Create NSManagedObject Subclass」をクリック
  2. 作成するEntityを選択し、アプリのフォルダ内に「create」する
  3. 「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を使う準備ができたので、見た目の部分と動的にデータを作成・編集・保存するためのファイルを作ります。
全部一つのファイルにコードを書くとごちゃごちゃしてしまうので、いくつかファイル分けします。

編集・作成したファイル
  • [編集]ContentView.swift
    データをリスト表示/新規作成ボタンを備えたトップページ
  • [作成]SampleCardVIew.swift
    データをリスト表示するための雛形のSwiftUIViewファイル
  • [作成]SheetView.swift
    データを入力するSheetのSwiftUIViewファイル
  • [作成]SampleModel.swift
    入力したデータの処理をまとめたModelファイル

フォルダ分けしてこんな感じのファイル構造になってます。
ファイルとフォルダは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で複数枚の写真をカメラロールから追加する方法をまとめます。
つづく!

タイトルとURLをコピーしました