SwiftDataとSwiftUIを使ってTODOリストアプリを作る

Shingo Hiraya
スタディスト Tech Blog
9 min readJun 16, 2023

--

はじめまして。開発本部でiOSアプリの開発を担当しております、平屋です。

本記事は2023年6月スタディスト「テックブログ月間」の6月16日の記事です。先週開催されたWWDC23で発表された新しいフレームワーク「SwiftData」の使用例を紹介していきます。

SwiftData

SwiftDataはiOS 17以降で利用可能で、Platforms State of the Unionでは以下のように説明されています。

  • データモデリングと管理のためのフレームワーク
  • Core Dataで実証済みの永続化レイヤーの上に構築
  • API は完全に再設計され、Swift向けに再考
  • Swiftの新しいマクロシステムを使用して、合理化されたAPIを提供
  • Core Dataにあるようなモデルファイル的なものは作成不要

また、SwiftDataはSwiftUIを念頭に置いて設計されているため、SwiftUIと組み合わせて使用するとより効率的にアプリを実装できるそうです。なので、今回はSwiftUIと組み合わせた実装を試してみました。

サンプルアプリの実装を見ていきながら、SwiftDataの使用例を紹介していきます。

サンプルアプリの概要

今回扱うサンプルアプリは「TODOリストアプリ」で、画面構成や機能は以下の通りです。

タスク一覧画面:

  • タスク一覧を表示
  • 特定のタスクを削除
  • 特定のタスクを完了にする

タスク作成画面:

  • 新規タスクを作成

タスク編集画面:

  • 既存のタスクのタイトルを修正

現時点のXcode 15はbeta版なので、スクリーンショットを載せるのは控えます。

また、サンプルアプリの実装は、重要な箇所を中心に解説していきます。サンプルアプリ全体のコードを見たい方は、以下のリポジトリをご覧ください。

検証環境

サンプルアプリ実装に使用した環境は以下の通りです。

  • macOS Ventura 13.4
  • Xcode Version 15 beta

(1) Model

では、実装を紹介していきます。まずはModelについてです。

SwiftDataに対応するのに最低限必要なのは、SwiftDataをimportし、@Modelをつけることです。これによってスキーマ定義が完了し、storedなプロパティは永続化対象になります。

// SwiftDataをimport
import SwiftData

// @Modelをつける
@Model
final class Task {
var title: String
var finished = false
// ...
}

(2) ModelContainerとModelContext

次に、Modelを操作するための主要なオブジェクトの概要とセットアップ方法を紹介します。

  1. ModelContainer
  2. ModelContext

(2–1) ModelContainer

ModelContainerはModelの永続化のバックエンドを提供します。

SwiftUIと組み合わせる場合、SceneまたはViewのmodifierを使ってセットアップできます。modifierの引数には扱いたいモデルを指定します。

今回のサンプルアプリでは、Sceneに対してセットアップします。(ViewごとにModelContainerをセットアップすることもできるようです。)

@main
struct SwiftDataSampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// 扱いたいモデルを指定してModelContainerをセットアップ
.modelContainer(for: Task.self)
}
}

(2–2) ModelContext

ModelContextはモデルへの全ての変更を監視し、それらを操作するためのアクションを提供します。

  • アクション: データの取得、変更の保存、変更の追跡、変更の取り消しなど

modifierを使ってModelContainerをセットアップ済みの場合、ModelContextはEnvironmentから取得できます。

struct AddTaskView: View {
// ModelContextはEnvironmentから取得できる
@Environment(\.modelContext) private var modelContext

// ...
}

(3) データに対する操作

ModelContainerとModelContextをセットアップできたので、次に、データに対する各操作を紹介していきます。

(3–1) 保存

データを保存するには、ModelContext.insert(object:)を使います。

以下のコード例は、タスクを追加する画面の実装です。「保存」ボタンタップでTaskを保存します。

struct AddTaskView: View {
// ...

var body: some View {
TextField("やることを入力", text: $task.title)
.padding()
.navigationBarItems(
trailing: Button(action: {
// Taskを保存する
modelContext.insert(object: task)

// ...
}) {
Text("保存")
}
)
}
}

(3–2) 一覧

SwiftUIと組み合わせる場合、データの一覧を取得するには@Queryプロパティラッパーを使用します。

以下のコード例は、タスク一覧を表示する画面の実装です。タスクのタイトル一覧がリストに表示されます。

struct ContentView: View {
// @Queryを使ってTask一覧を取得
@Query private var tasks: [Task]

// ...

var body: some View {
NavigationStack {
List {
ForEach(Array(tasks.enumerated()), id: \.offset) { index, task in
HStack {
// ...

// Taskのタイトルを表示
NavigationLink(task.title) {
EditTaskView(task: task)
}
}
}
// ...
}
// ...
}
}
// ...
}

(3–3) 更新

SwiftUIと組み合わせる場合、プロパティの値の変更は自動で永続化されるようです。

以下のコード例は、ボタンタップでfinished(タスク完了状態を表す)プロパティの値をトグルするものです。

Button {
task.finished.toggle()
} label: {
// ...
}

(3–4) 削除

データを保存するには、ModelContext.delete(_:)を使います。

以下のコード例は、タスク一覧を表示する画面において、セルスワイプで削除操作を行う実装です。

struct ContentView: View {
// ...

var body: some View {
NavigationStack {
List {
ForEach(Array(tasks.enumerated()), id: \.offset) { index, task in
// ...
}
// 削除操作と処理を紐付ける
.onDelete(perform: deleteItems)
}
// ...
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
// Taskを削除する
modelContext.delete(tasks[index])
}
}
}
}

(4) その他

(4–1) SwiftUIプレビュー用のデータ

SwiftUIのプレビューを使いながら実装する場合、固定のデータがあったほうが良いですよね。

以下のようにインメモリのModelContainerを作成してプレビュー用のビューに適用すれば固定データの表示を実現できます。

#Preview {
ContentView()
.modelContainer(previewContainer)
}

@MainActor
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer(
for: Task.self, ModelConfiguration(inMemory: true)
)
let sampleTasks = [
Task(title: "コードレビューを行う"),
Task(title: "バグ修正を行う"),
Task(title: "ドキュメントを更新する")]
for task in sampleTasks {
container.mainContext.insert(object: task)
}
return container
} catch {
// ...
}
}()

さいごに

本記事ではSwiftDataの使用例を紹介してきました。直感的かつ少ないコードでデータの永続化を実現できますね。CoreDataやサードパーティのDBフレームワークは学習コストやメンテナンスコストが高い印象があるので、こういうフレームワークが(しかも組み込みで)登場するのを期待していた方は多かったのではないでしょうか。

現時点ではまだSwiftDataの一部しか触れていないので、引き続き使い倒してみたいと思います。

We’re hiring!

スタディストでは一緒に“知的活力みなぎる社会”をつくるメンバーを、広く募集しています! Entrance Bookもあるので覗いてみてください。

参考資料

--

--