はじめに
こんにちは、iOS開発でよくローカルデータベースを使っています、ionishiです。
これまで、SwiftDataに関する記事を何度か書いてきました!
SwiftDataは、@Queryマクロを使うことで、かなり少ない実装でデータの永続化を行うことができる一方で、SwiftUIのView内でしか利用できないという制約から責務の分離やテスタブルな設計を行うことが難しいです
結果として、アプリがスケールしづらくなることに課題を感じています。
そんなSwiftDataの悩みを解消するかのようなOSS、sqlite-dataが、TCAで有名なpoint-freeより公開されています。
2025年9月17日にバージョン1系が公開されたばかりの、比較的新しいOSSです。
この記事では、このsqlite-dataで簡単なTodoリストアプリを実装しながら、SwiftDataと比較した利点と欠点を考えてみたいと思います。
環境
以下の環境、バージョンを対象としています
- Xcode: 26.1
- sqlite-data: 1.4.0
TL:DR
-
SwiftDataと比べて、マクロによる永続化データ取得の柔軟性が高く、SwiftUIのView以外からでもマクロで取得できる。そのため、マクロを利用しても責務を適切に分離しやすい - アプリ全体が
sqlite-dataに依存するため、破壊的変更やアップデート頻度の影響が大きい
sqlite-dataとは
sqlite-dataはpoint-freeが公開している、GRDB.swiftをベースにしたSQLiteのラッパーライブラリです。
リポジトリのREADMEには、以下のような記載があります
SQLiteData is a fast, lightweight replacement for SwiftData, including CloudKit synchronization (and even CloudKit sharing), built on top of the popular GRDB library. To populate data from the database you can use @Table and @FetchAll, which are similar to SwiftData's @Model and @Query
(サンプルコード省略)
Both of the above examples fetch items from an external data store using Swift data types, and both are automatically observed by SwiftUI so that views are recomputed when the external data changes, but SQLiteData is powered directly by SQLite and is usable from UIKit, @Observable models, and more.
ObservableなクラスやUIKitからも@FetchAllマクロなどが利用できることが強調されており、SwiftDataの@Queryマクロの課題に対する回答となるのではないかという期待が高まりますね
sqlite-dataを使ってTodoアプリを実装してみる
簡単なTodoリストアプリを作りながら、sqlite-dataの実装を見ていきたいと思います
データベース、テーブルの作成
SQLiteがベースとなっているため、当然DBやテーブルの作成が必要です。
今回は、公式のサンプルコードに則りApp構造体内で作成することにします
SwiftDataで、modelContainerを設定する部分に似ているという記載がREADMEにありますが、SwiftDataよりは細かな設定が必要な印象です
@main
struct sqlite_data_sampleApp: App {
init() {
prepareDependencies {
let databasePath = URL.documentsDirectory
.appending(path: "todos.sqlite")
.path()
let database = try! DatabaseQueue(path: databasePath)
var migrator = DatabaseMigrator()
migrator.registerMigration("Create todos table") { db in
try #sql(
"""
CREATE TABLE "todos" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"isCompleted" INTEGER NOT NULL DEFAULT 0,
"createdAt" REAL NOT NULL
)
"""
)
.execute(db)
}
try! migrator.migrate(database)
$0.defaultDatabase = database
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Model
永続化モデルとして、Todoという構造体を作ります
@Table
struct Todo: Equatable, Identifiable, Sendable {
let id: Int
var title: String
var isCompleted: Bool = false
var createdAt: Double = Date().timeIntervalSince1970
}
SwiftDataで@Modelを利用するパターンと非常に似ています
SwiftDataの場合はclassを利用しますが、sqlite-dataではstructで良いようですね
Repository
DBにアクセスするRepositoryを作ります
defaultDatabaseが@Dependencyマクロで取得できます
どうやら、swift-dependenciesを内部で利用しているようですね
database.writeでトランザクションを作り、モデルに対してfind、insert、deleteなどを実行後、execute(db)で永続化層に反映できるようです
また、以下のコード例にはありませんが、以下のようにsqlを使った複雑なデータ取得も可能です
@FetchAll(#sql("SELECT * FROM todos WHERE ……")
これは、SwiftDataにはないsqlite-dataの強みになりそうです
final class TodoRepository: Sendable {
@Dependency(\.defaultDatabase) private var database
func add(_ todo: Todo) async throws {
let draft = Todo.Draft(
title: todo.title,
isCompleted: todo.isCompleted,
createdAt: todo.createdAt
)
try await database.write { db in
_ = try Todo.insert { draft }
.execute(db)
}
}
func update(_ todo: Todo) async throws {
try await database.write { db in
_ = try Todo.update(todo)
.execute(db)
}
}
func delete(_ todo: Todo) async throws {
try await database.write { db in
_ = try Todo.find(todo.id)
.delete()
.execute(db)
}
}
}
ViewModel
sqlite-dataの目玉の一つである、@ObservaleをつけたViewModelからの利用です
SwiftDataの@QueryマクロはSwiftUIのViewからしか利用できませんが、@FetchAllマクロがViewModelから利用できています!
@Observableをつけたクラスでの利用時には、@FetchAllマクロと合わせて@ObservationIngoredを付ける必要があります。
@Observable
@MainActor
final class TodoViewModel {
@ObservationIgnored
@FetchAll(Todo.order { $0.createdAt.desc() })
var todos: [Todo]
var newTodoTitle: String = ""
var errorMessage: String?
private let repository: TodoRepository
init(repository: TodoRepository) {
self.repository = repository
}
func addTodo() async {
let title = newTodoTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty else { return }
let todo = Todo(id: 0, title: title)
do {
try await repository.add(todo)
newTodoTitle = ""
} catch {
errorMessage = error.localizedDescription
}
}
func toggleCompletion(for todo: Todo) async {
var updatedTodo = todo
updatedTodo.isCompleted.toggle()
do {
try await repository.update(updatedTodo)
} catch {
errorMessage = error.localizedDescription
}
}
func delete(_ todo: Todo) async {
do {
try await repository.delete(todo)
} catch {
errorMessage = error.localizedDescription
}
}
}
TodoView
Viewからは、ViewModelのTodoプロパティを読み込んでUIをつくるのみです!
struct TodoView: View {
@State private var viewModel = TodoViewModel(repository: TodoRepository())
var body: some View {
NavigationStack {
List {
Section {
HStack {
TextField("New TODO", text: $viewModel.newTodoTitle)
.textFieldStyle(.roundedBorder)
Button {
Task {
await viewModel.addTodo()
}
} label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
}
.disabled(viewModel.newTodoTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
Section {
ForEach(viewModel.todos) { todo in
HStack {
Button {
Task {
await viewModel.toggleCompletion(for: todo)
}
} label: {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(todo.isCompleted ? .green : .gray)
}
.buttonStyle(.plain)
Text(todo.title)
.strikethrough(todo.isCompleted)
.foregroundStyle(todo.isCompleted ? .secondary : .primary)
Spacer()
}
}
.onDelete { indexSet in
Task {
for index in indexSet {
await viewModel.delete(viewModel.todos[index])
}
}
}
}
}
.navigationTitle("TODO")
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
}
}
}
}
}
SwiftDataとsqlite-dataの比較
様々な観点から、SwiftDataとsqlite-dataを比較してみます
実装難易度
実装の難易度に関しては、どちらのライブラリも非常に簡単に利用できると感じました。
sqlite-dataがSwiftDataを意識して作られていることもあり、SwiftDataの利用経験やSQLの知識があればすぐに使い始められると思います
責務分離のしやすさ
sqlite-dataでは、@FetchAllなどのマクロを使っても責務を適切に分離しやすいです
やはりObservableなクラス内で@FetchAllなどのマクロが利用できるため、シンプルな手段でViewから永続化データのプロパティを引き剥がせます
SwiftDataでは、これの実現のために、fetchメソッドを使って手動で永続化データを取得する必要があります。
また、リアクティブに永続化データを反映するために、独自の対応が必要になることもあり、実装が膨らみがちです。
sqlite-dataはマクロを使いながらも責務を分離できるという、SwiftDataでは選べない中間的な選択肢となりそうです。
ライブラリの安定性
sqlite-dataはOSSです。
ファーストパーティであるAppleが提供するSwiftDataに比べて、継続的なメンテナンスへの懸念があります。
特に、sqlite-dataの場合には、プレゼンテーション層でmodelをマクロで取り出す都合上、アプリ全体がsqlite-dataに依存する状況になります。
sqlite-dataから別のライブラリへの変更は、比較的大きな工数が必要になるのではないかと感じています。
また、sqlite-dataはまだリリースして1年にも満たないOSSですから、破壊的な変更が入る可能性も高いです。
ベースとしてはSQLiteを利用しているため、永続化データが消えるようなことはなさそうですが、マクロの仕様等が大きく変更される可能性は十分にありえると思います。
現状は継続的なアップデートがなされていますが、今後これが継続される保証はありません。
ライブラリとしての安定性の面では、現時点ではApple製であるSwiftDataの方が安心して利用できそうだと感じています
まとめ
今回は、point-freeから公開されているsqlite-dataを使ってみました
SwiftDataと比べて、マクロを利用した際の実装の柔軟性が大きく上がっている点が強みと言えそうです
SQLiteラッパーなので、SQLiteやSQLの知識を必要としますが、実装の量を抑えながら永続化データをリアクティブに扱うことができます
一方で、OSSという点やライブラリとして枯れておらず、Apple製のSwiftDataと比べると破壊的な変更や継続的なアップデートに対するリスクが高いです。
プレゼンテーション層から永続仮想までsqlite-dataに依存してしまうと、対応に苦労する場面もありそうです
sqlite-dataを優先して利用したい場面としては、以下のようなときかなと感じています
- マクロを使ってシンプルに永続化データを扱いたい
- ViewModelのテストを書くなどの理由で、Viewから永続化データを分離したい
- 小規模なアプリの場合など、
sqlite-dataやOSのアップデートへの追従が比較的容易な状況 - SQLによる複雑な永続化データの処理を行いたい