💽

SwiftUI × CoreDataでCRUD機能を持つ簡単なメモアプリを作ってみた

2022/03/21に公開

CoreDataとは、1つのデバイスでデータを永続化したり複数のデバイスでデータを同期させることのできるフレームワークのことです。
https://developer.apple.com/documentation/coredata
軽量なデータであればUserDefaultsに保存でも問題ないですが、大きなデータだったりデータ間の関連付けをしたい場合はUserDefaultsよりCoreDataなどのフレームワークを導入した方がいいケースが多いです。

最近CoreDataとSwiftUIは相性が良いという噂を聞いたため、実際に使ってみようと思いCRUD機能を持つ簡単なメモアプリを作ってみました!

環境
Swift5
Xcode13.1
iOS15

1. プロジェクトを作成する

新規でプロジェクトを作成します。
Choose optionsfor your projectのタイミングで「SwiftUI」を指定し、「Use Core Data」にチェックを入れてください(あとで追加も可能です)

2. データベースを設定する

最初にEnitityを設定します。
「xxx.xcdatamodeld」を操作してください。

  1. デフォルトで追加されているItemを選択し、deleteする
  2. 「Add Entity」をクリック
  3. Entity名を「Memo」に変更しAttributeを追加する
Attribute Type
title String
content String
createdAt Date
updatedAt Date

  1. EntityのCodegenを「Manual/None」にする
  2. 「Editor」 -> 「Create NSManagedObject Subclass」を選択する
  3. 作成するEntityにチェックが入っていることを確認して、「create」する

これでデータベースの準備ができました。

メモ一覧の表示の際にupdatedAtのString型が必要になるため、前もって準備しておきます。
"Memo+CoreDataProperties.swift"に書くことで、毎回型変換する必要がなく欲しい値を呼び出すことができるようになります。

//  Memo+CoreDataProperties.swift

import Foundation
import CoreData


extension Memo {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Memo> {
        return NSFetchRequest<Memo>(entityName: "Memo")
    }

    @NSManaged public var title: String?
    @NSManaged public var content: String?
    @NSManaged public var createdAt: Date?
    @NSManaged public var updatedAt: Date?

}

extension Memo : Identifiable {
    // ここに追加
    // stringUpdatedAtを呼び出すとString型のupdatedAtが返却される
    public var stringUpdatedAt: String { dateFomatter(date: updatedAt ?? Date()) }
    
    func dateFomatter(date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

        return dateFormatter.string(from: date)
    }
}

3. メモアプリを実装をする

データベースの準備ができましたので、ここからCRUD機能を持ったメモアプリを作っていきます。

Persistence.swiftの中身を書き換える

実装しながらPreviewで確認する時に利用されるデータを準備します。
このデータは"Persistence.swift"内で設定しています。
static var preview: PersistenceController = {...}の中身を書き換えてください。
※初期ではデータベースの設定時に削除したItem Entity用のデータが生成されてます。

for _ in 0..<10 {
    let newItem = Item(context: viewContext)
    newItem.timestamp = Date()
}

を以下に変更します。

for index in 0..<10 {
    let newMemo = Memo(context: viewContext)
    newMemo.title = "メモタイトル\(index + 1)"
    newMemo.content = "メモ\(index + 1)の内容が記載されています"
    newMemo.createdAt = Date()
    newMemo.updatedAt = Date()
}

メモ一覧を表示する

続いて、メモ一覧画面を実装します。
"ContentView.swift"を編集してください。

ポイントとしては、@FetchRequestというプロパティラッパーを使うことで、データの値をリアルタイムで取得・表示することができます。

//  ContentView.swift

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        entity: Memo.entity(),
        sortDescriptors: [NSSortDescriptor(key: "updatedAt", ascending: false)],
        animation: .default
    ) var fetchedMemoList: FetchedResults<Memo>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(fetchedMemoList) { memo in
                    VStack {
                        Text(memo.title ?? "")
                           .font(.title)
                           .frame(maxWidth: .infinity,alignment: .leading)
                           .lineLimit(1)
                       HStack {
                           Text (memo.stringUpdatedAt)
                               .font(.caption)
                               .lineLimit(1)
                           Text(memo.content ?? "")
                               .font(.caption)
                               .lineLimit(1)
                           Spacer()
                       }
                    }
                }
            }
            .navigationTitle("メモ")
            .navigationBarTitleDisplayMode(.automatic)
        }
    }
}

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

Previewを確認してこう見えていたらOKです。

この時点ではデータ追加ができないため、シュミレータや実機で確認してもナビゲーションタイトルだけが表示されます。

メモの新規追加を実装する

続いて、新規追加機能を実装します。
"AddMemoView.swift"というファイルを作成します。
ここでは、保存ボタンを押下したら新たなMemoインスタンスを生成し、入力した内容をデータベースに保存する処理を行なっています。(addMemoメソッド)

//  AddMemoView.swift

import SwiftUI

struct AddMemoView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.presentationMode) var presentation
    @State private var title: String = ""
    @State private var content: String = ""

    var body: some View {
        VStack {
            TextField("タイトル", text: $title)
                .font(.title)
            TextEditor(text: $content)
                .font(.body)
            Spacer()
        }
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button(action: {addMemo()}) {
                    Text("保存")
                }
            }
        }
    }
    // 保存ボタン押下時の処理
    private func addMemo() {
        let memo = Memo(context: viewContext)
        memo.title = title
        memo.content = content
        memo.createdAt = Date()
        memo.updatedAt = Date()
	// 生成したインスタンスをCoreDataに保存する
        try? viewContext.save()
	
        presentation.wrappedValue.dismiss()
    }
}

struct AddMemoView_Previews: PreviewProvider {
    static var previews: some View {
        AddMemoView()
    }
}

メモ一覧画面から新規追加画面に遷移する動線を作ります。

//  ContentView.swift

    var body: some View {
        NavigationView {
            List {...}
            .navigationTitle("メモ")
            .navigationBarTitleDisplayMode(.automatic)
	    // ここに追加!
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink(destination: AddMemoView()) {
                        Text("新規作成")  
                    }
                }
            }
        }
    }

メモ一覧画面で新規作成ボタンをタップすることでデータの追加ができるようになります。
ここまできたらPreviewでなく実際にビルドしても確認ができるようになります。

作成した新規追加画面がこちら。

メモの削除を実装する

続いては削除機能です。
今回はメモ一覧画面からスワイプすることで削除できるようにします。

//  ContentView.swift

    var body: some View {
        NavigationView {
            List {
                ForEach(fetchedMemoList) { ... }
		// ここに書く
                .onDelete(perform: deleteMemo)
            }
              // 略
        }
    }
    
    // 削除時の処理
    private func deleteMemo(offsets: IndexSet) {
        offsets.forEach { index in
            viewContext.delete(fetchedMemoList[index])
        }
	// 保存を忘れない
        try? viewContext.save()
    }

これでスワイプ削除できるようになります。

メモの編集を実装する

最後に編集機能を追加します。
新規追加と画面が似ていますが、新たに"EditMemoView"ファイルを作ります。

//  EditMemoView.swift

import SwiftUI

struct EditMemoView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @State private var title: String
    @State private var content: String
    private var memo: Memo
    
    init(memo: Memo) {
        self.memo = memo
        self.title = memo.title ?? ""
        self.content = memo.content ?? ""
    }
    
    var body: some View {
        VStack {
            TextField("タイトル", text: $title)
                .font(.title)
            TextEditor(text: $content)
                .font(.body)
            Spacer()
        }
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button(action: {saveMemo()}) {
                    Text("保存")
                }
            }
        }
    }
    
    private func saveMemo() {
        memo.title = title
        memo.content = content
        memo.updatedAt = Date()

        try? viewContext.save()
    }
}

struct EditMemoView_Previews: PreviewProvider {
    static var previews: some View {
        EditMemoView(memo: Memo())
    }
}

ListとNavigationLinkを組み合わせて、メモ一覧画面から編集画面に遷移できるようにします。

//  ContentView.swift

    var body: some View {
        NavigationView {
            List {
                ForEach(fetchedMemoList) { memo in
		    // VStackをNavigationLinkで囲み、遷移先を指定する
                    NavigationLink(destination: EditMemoView(memo: memo)) {
                        VStack { ... }
                    }
                }
		// 以下略

メモ一覧画面から各リストをタップすることで編集画面に遷移することができます。
画面編集をしてからメモ一覧に戻ることで、データが更新されていることがわかると思います。
保存時にupdatedAtも現在日時に更新してます。
メモ一覧ではupdatedAtを降順で表示をしているため、更新されたデータが一番上になってることを確認できたらアプリの完成です!

最終的なソースコードはGitHubにありますので適宜ご覧ください!
https://github.com/tomsan96/MemoApp
指摘等ありましたらコメント等いただけたら嬉しいです!

※追記
検索機能を追加した記事も書きました!
https://zenn.dev/tomsan96/articles/0a76b9b457dc6f

参考

https://qiita.com/pe-ta/items/89203ccc72d964277818
https://capibara1969.com/3178/
https://tomato-develop.com/swiftui-basic-how-to-use-coredata-crud/

Discussion