SQLiteData Usage Guide
SQLiteData is a fast, lightweight replacement for SwiftData from Point-Free, powered by SQL and supporting CloudKit synchronization (and even CloudKit sharing). It is built on top of GRDB and StructuredQueries.
Key Dependencies
- GRDB: The underlying SQLite interface library. Used for database connections, transactions, migrations, and observation.
- StructuredQueries: Provides the
@Tablemacro and type-safe query building APIs. - Swift Dependencies: Used for dependency injection (
@Dependency,prepareDependencies).
Table of Contents
- Defining Your Schema
- Preparing the Database
- Fetching Data
- Observing Changes
- Dynamic Queries
- CRUD Operations
- Associations & Joins
- CloudKit Synchronization
- Testing & Previews
- Complete Examples
1. Defining Your Schema
Use the @Table macro from StructuredQueries to define your data types. Unlike SwiftData's @Model (which requires classes), @Table works with structs.
Basic Table Definition
import SQLiteData
@Table
struct Item: Identifiable {
let id: Int // Primary key (auto-generated for Int)
var title = ""
var isInStock = true
var notes = ""
}
UUID Primary Key
@Table
struct RemindersList: Identifiable {
let id: UUID
var title = ""
var position = 0
}
Custom Column Mapping
@Table
struct RemindersList: Hashable, Identifiable {
let id: UUID
@Column(as: Color.HexRepresentation.self)
var color: Color = Self.defaultColor
var position = 0
var title = ""
}
Custom Primary Key
@Table
struct Tag: Hashable, Identifiable {
@Column(primaryKey: true)
var title: String
var id: String { title }
}
Enums in Tables
Enums must conform to QueryBindable:
@Table
struct Reminder: Identifiable {
let id: UUID
var priority: Priority?
var status: Status = .incomplete
enum Priority: Int, QueryBindable {
case low = 1
case medium
case high
}
enum Status: Int, QueryBindable {
case incomplete = 0
case completed = 1
case completing = 2
}
}
Computed Query Expressions
You can define computed query expressions on your table's TableColumns:
nonisolated extension Reminder.TableColumns {
var isCompleted: some QueryExpression<Bool> {
status.neq(Reminder.Status.incomplete)
}
var isPastDue: some QueryExpression<Bool> {
@Dependency(\.date.now) var now
return !isCompleted && #sql("coalesce(date(\(dueDate)) < date(\(now)), 0)")
}
var isToday: some QueryExpression<Bool> {
@Dependency(\.date.now) var now
return !isCompleted && #sql("coalesce(date(\(dueDate)) = date(\(now)), 0)")
}
var isScheduled: some QueryExpression<Bool> {
!isCompleted && dueDate.isNot(nil)
}
}
Pre-defined Query Shortcuts
extension Reminder {
static let incomplete = Self.where { !$0.isCompleted }
static let withTags = group(by: \.id)
.leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) }
.leftJoin(Tag.all) { $1.tagID.eq($2.primaryKey) }
}
@Selection Macro for Custom Result Types
Use @Selection for custom types that hold the results of joins or partial selects:
@Selection
struct ReminderListState: Identifiable {
var id: RemindersList.ID { remindersList.id }
var remindersCount: Int
var remindersList: RemindersList
@Column(as: CKShare?.SystemFieldsRepresentation.self)
var share: CKShare?
}
@Selection
struct Stats {
var allCount = 0
var flaggedCount = 0
var scheduledCount = 0
var todayCount = 0
}
Junction Tables (Many-to-Many)
@Table("remindersTags")
struct ReminderTag: Identifiable {
let id: UUID
let reminderID: Reminder.ID
let tagID: Tag.ID
}
Full-Text Search (FTS5)
@Table
struct ReminderText: FTS5 {
let rowid: Int
let title: String
let notes: String
let tags: String
}
2. Preparing the Database
Step 1: Create appDatabase() Function
import OSLog
import SQLiteData
func appDatabase() throws -> any DatabaseWriter {
@Dependency(\.context) var context
var configuration = Configuration()
// Optional: Enable query tracing for debugging
#if DEBUG
configuration.prepareDatabase { db in
db.trace(options: .profile) {
if context == .preview {
print("\($0.expandedDescription)")
} else {
logger.debug("\($0.expandedDescription)")
}
}
}
#endif
// Create database (auto-provisions unique temp DBs for previews/tests)
let database = try defaultDatabase(configuration: configuration)
logger.info("open '\(database.path)'")
// Migrate
var migrator = DatabaseMigrator()
#if DEBUG
migrator.eraseDatabaseOnSchemaChange = true
#endif
migrator.registerMigration("Create tables") { db in
try #sql("""
CREATE TABLE "items" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL DEFAULT '',
"isInStock" INTEGER NOT NULL DEFAULT 1,
"notes" TEXT NOT NULL DEFAULT ''
) STRICT
""")
.execute(db)
}
// Register more migrations as your app evolves
migrator.registerMigration("Add 'description' column") { db in
try #sql("""
ALTER TABLE "items"
ADD COLUMN "description" TEXT
""")
.execute(db)
}
try migrator.migrate(database)
return database
}
private let logger = Logger(subsystem: "MyApp", category: "Database")
Step 2: Set Default Database in App Entry Point
SwiftUI:
import SQLiteData
import SwiftUI
@main
struct MyApp: App {
init() {
prepareDependencies {
$0.defaultDatabase = try! appDatabase()
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
UIKit AppDelegate:
class AppDelegate: NSObject, UIApplicationDelegate {
func applicationDidFinishLaunching(_ application: UIApplication) {
prepareDependencies {
$0.defaultDatabase = try! appDatabase()
}
}
}
Bootstrap Pattern (Recommended for Multiple Dependencies)
Group database + sync engine setup:
extension DependencyValues {
mutating func bootstrapDatabase() throws {
defaultDatabase = try appDatabase()
defaultSyncEngine = try SyncEngine(
for: defaultDatabase,
tables: RemindersList.self, Reminder.self, Tag.self, ReminderTag.self
)
}
}
// In App entry point:
@main
struct MyApp: App {
init() {
try! prepareDependencies {
try $0.bootstrapDatabase()
}
}
// ...
}
3. Fetching Data
@FetchAll — Fetch a Collection
Fetch all rows with default ordering:
@FetchAll var items: [Item]
With ordering:
@FetchAll(Item.order(by: \.title))
var items
With ordering by multiple columns:
@FetchAll(Item.order(by: \.isInStock, \.title))
var items
With filtering:
@FetchAll(Reminder.where(\.isCompleted).order { $0.title.desc() })
var completedReminders
With ordering by multiple columns using a closure:
@FetchAll(
Item.order {
($0.isInStock,
$0.title)
}
)
var items
With mixed sort directions across multiple columns:
@FetchAll(
Item.order {
($0.isInStock,
$0.title.desc())
}
)
var items
With animation:
@FetchAll(Item.order { $0.id.desc() }, animation: .default)
private var items
Using SQL string (#sql macro):
@FetchAll(#sql("SELECT * FROM reminders WHERE isCompleted ORDER BY title DESC"))
var completedReminders: [Reminder]
Using schema-safe SQL interpolation:
@FetchAll(
#sql("""
SELECT \(Reminder.columns)
FROM \(Reminder.self)
WHERE \(Reminder.isCompleted)
ORDER BY \(Reminder.title) DESC
""")
)
var completedReminders: [Reminder]
@FetchOne — Fetch a Single Value
Count:
@FetchOne(Reminder.count())
var remindersCount = 0
Filtered count:
@FetchOne(Reminder.where(\.isCompleted).count())
var completedRemindersCount = 0
Aggregate computation with @Selection:
@FetchOne(
Reminder.select {
Stats.Columns(
allCount: $0.count(filter: !$0.isCompleted),
flaggedCount: $0.count(filter: $0.isFlagged && !$0.isCompleted),
scheduledCount: $0.count(filter: $0.isScheduled),
todayCount: $0.count(filter: $0.isToday)
)
}
)
var stats = Stats()
@Fetch — Multiple Queries in One Transaction
Define a FetchKeyRequest:
struct Facts: FetchKeyRequest {
struct Value {
var facts: [Fact] = []
var count = 0
}
func fetch(_ db: Database) throws -> Value {
try Value(
facts: Fact.order { $0.id.desc() }.fetchAll(db),
count: Fact.fetchCount(db)
)
}
}
Use it:
@Fetch(Facts(), animation: .default)
private var facts = Facts.Value()
// Access:
facts.facts // [Fact]
facts.count // Int
Joins and Custom Selections
@FetchAll(
RemindersList
.group(by: \.id)
.order(by: \.position)
.leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) && !$1.isCompleted }
.leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) }
.select {
ReminderListState.Columns(
remindersCount: $1.id.count(),
remindersList: $0,
share: $2.share
)
},
animation: .default
)
var remindersLists
4. Observing Changes
SwiftUI Views
Property wrappers automatically observe database changes and re-render:
struct ItemsView: View {
@FetchAll var items: [Item]
var body: some View {
ForEach(items) { item in
Text(item.name)
}
}
}
@Observable Models
Important: Must annotate with
@ObservationIgnoreddue to macro interactions; SQLiteData handles its own observation.
@Observable
@MainActor
class ItemsModel {
@ObservationIgnored
@FetchAll(Item.order { $0.id.desc() }, animation: .default)
var items
@ObservationIgnored
@FetchOne(Item.count(), animation: .default)
var itemsCount = 0
@ObservationIgnored
@Dependency(\.defaultDatabase) var database
func deleteItem(indices: IndexSet) {
withErrorReporting {
try database.write { db in
let ids = indices.map { items[$0].id }
try Item
.where { $0.id.in(ids) }
.delete()
.execute(db)
}
}
}
}
UIKit View Controllers
Use $items.publisher (Combine) or observe (Swift Navigation):
class ItemsViewController: UICollectionViewController {
@FetchAll(Fact.order { $0.id.desc() }, animation: .default)
private var facts
override func viewDidLoad() {
super.viewDidLoad()
// Using Swift Navigation's observe:
observe { [weak self] in
guard let self else { return }
var snapshot = NSDiffableDataSourceSnapshot<Section, Fact>()
snapshot.appendSections([.facts])
snapshot.appendItems(facts, toSection: .facts)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
5. Dynamic Queries
Use $items.load(...) to dynamically change the query based on user input:
In a SwiftUI View
struct ContentView: View {
@FetchAll var items: [Item]
@State var filterDate: Date?
@State var order: SortOrder = .reverse
var body: some View {
List { /* ... */ }
.task(id: [filterDate, order] as [AnyHashable]) {
await updateQuery()
}
}
private func updateQuery() async {
do {
try await $items.load(
Item
.where { $0.timestamp > #bind(filterDate ?? .distantPast) }
.order {
if order == .forward {
$0.timestamp
} else {
$0.timestamp.desc()
}
}
.limit(10)
)
} catch { /* Handle error */ }
}
}
Dynamic Query with @Fetch
@Fetch(Facts(), animation: .default) private var facts = Facts.Value()
@State var query = ""
var body: some View {
List { /* ... */ }
.searchable(text: $query)
.task(id: query) {
await withErrorReporting {
try await $facts.load(Facts(query: query), animation: .default).task
}
}
}
private struct Facts: FetchKeyRequest {
var query = ""
struct Value {
var facts: [Fact] = []
var searchCount = 0
var totalCount = 0
}
func fetch(_ db: Database) throws -> Value {
let search = Fact
.where { $0.body.contains(query) }
.order { $0.id.desc() }
return try Value(
facts: search.fetchAll(db),
searchCount: search.fetchCount(db),
totalCount: Fact.fetchCount(db)
)
}
}
Important: If a parent view refreshes, a dynamically-updated query can be overwritten with the initial
@FetchAll's value. Use@State @FetchAllto keep query state local to the view.
Pagination
Offset-based Pagination
Use .limit(_:offset:) to fetch a specific page of results:
let pageSize = 20
let page = 2 // 0-indexed
@FetchAll(
Item
.order { $0.title }
.limit(pageSize, offset: page * pageSize)
)
var items
Dynamically load a page in response to user input:
struct ItemsView: View {
@State @FetchAll var items: [Item]
@State var page = 0
let pageSize = 20
var body: some View {
List(items) { item in Text(item.title) }
HStack {
Button("Previous") { page = max(0, page - 1) }
Button("Next") { page += 1 }
}
.task(id: page) {
await withErrorReporting {
try await $items.load(
Item
.order { $0.title }
.limit(pageSize, offset: page * pageSize)
)
}
}
}
}
Cursor (Keyset) Pagination
Keyset pagination uses the last seen value as a cursor, which is more efficient and stable than offset pagination for large datasets:
// First page — no cursor
@FetchAll(
Item
.order { $0.id }
.limit(20)
)
var items
// Next page — pass the last seen id as cursor
let lastID = items.last?.id
@FetchAll(
Item
.where { $0.id > #bind(lastID ?? 0) }
.order { $0.id }
.limit(20)
)
var nextItems
Dynamically load the next page inside a model:
@Observable
@MainActor
class ItemsModel {
@ObservationIgnored
@State @FetchAll var items: [Item]
@Dependency(\.defaultDatabase) var database
var lastID: Item.ID? = nil
func loadNextPage() async {
await withErrorReporting {
try await $items.load(
Item
.where { $0.id > #bind(lastID ?? 0) }
.order { $0.id }
.limit(20)
)
lastID = items.last?.id
}
}
}
6. CRUD Operations
All write operations go through the defaultDatabase dependency.
Access Database
@Dependency(\.defaultDatabase) var database
Create (Insert)
try database.write { db in
try Item.insert {
Item(id: UUID(), title: "New Item", isInStock: true, notes: "")
}
.execute(db)
}
Using Draft (auto-generated type without primary key):
try database.write { db in
try Fact.insert {
Fact.Draft(body: "Some fact text")
}
.execute(db)
}
Insert with defaults (using table defaults):
try database.write { db in
try Item.insert().execute(db)
}
Read (Fetch)
Use @FetchAll, @FetchOne, or @Fetch property wrappers (covered above).
For one-off reads within a write transaction:
try database.write { db in
let count = try Reminder.fetchCount(db)
let items = try Item.where(\.isInStock).fetchAll(db)
}
Update
Update an existing row:
existingItem.title = "New Title"
try database.write { db in
try Item.update(existingItem).execute(db)
}
Conditional update with query:
try database.write { db in
try Reminder
.where { $0.status.eq(#bind(.completing)) }
.update { $0.status = #bind(.completed) }
.execute(db)
}
Delete
Delete by value:
try database.write { db in
try Item.delete(existingItem).execute(db)
}
Delete by query:
try database.write { db in
try Item
.where { $0.id.in(idsToDelete) }
.delete()
.execute(db)
}
Delete with subquery:
try database.write { db in
try Tag
.where { $0.title.in(tagTitles) }
.delete()
.execute(db)
}
Batch Reordering
try database.write { db in
var ids = items.map(\.id)
ids.move(fromOffsets: source, toOffset: destination)
try Item
.where { $0.id.in(ids) }
.update {
let indexedIDs = Array(ids.enumerated())
let (first, rest) = (indexedIDs.first!, indexedIDs.dropFirst())
$0.position = rest
.reduce(Case($0.id).when(first.element, then: first.offset)) { cases, id in
cases.when(id.element, then: id.offset)
}
.else($0.position)
}
.execute(db)
}
7. Associations & Joins
SQLiteData does not use an ORM pattern. Instead, use SQL joins to efficiently fetch related data.
One-to-Many (Join + Group)
@FetchAll(
Sport
.group(by: \.id)
.leftJoin(Team.all) { $0.id.eq($1.sportID) }
.select {
SportWithTeamCount.Columns(sport: $0, teamCount: $1.count())
}
)
var sportsWithTeamCounts
Many-to-Many via Junction Table
extension Tag {
static let withReminders = group(by: \.primaryKey)
.leftJoin(ReminderTag.all) { $0.primaryKey.eq($1.tagID) }
.leftJoin(Reminder.all) { $1.reminderID.eq($2.id) }
}
// Use:
@FetchAll(
Tag
.order(by: \.title)
.withReminders
.having { $2.count().gt(0) }
.select { tag, _, _ in tag },
animation: .default
)
var tags
Three-Way Join
@FetchAll(
RemindersList
.group(by: \.id)
.order(by: \.position)
.leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) && !$1.isCompleted }
.leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) }
.select {
ReminderListState.Columns(
remindersCount: $1.id.count(),
remindersList: $0,
share: $2.share
)
},
animation: .default
)
var remindersLists
8. CloudKit Synchronization
Setup SyncEngine
@main
struct MyApp: App {
init() {
try! prepareDependencies {
$0.defaultDatabase = try appDatabase()
$0.defaultSyncEngine = try SyncEngine(
for: $0.defaultDatabase,
tables: RemindersList.self, Reminder.self, Tag.self
)
}
}
}
Attach Metadatabase
In your database configuration, attach the metadata database required for sync:
configuration.prepareDatabase { db in
try db.attachMetadatabase()
}
SyncEngine Delegate
@MainActor
@Observable
class MySyncEngineDelegate: SyncEngineDelegate {
var isDeleteLocalDataAlertPresented = false
func syncEngine(
_ syncEngine: SQLiteData.SyncEngine,
accountChanged changeType: CKSyncEngine.Event.AccountChange.ChangeType
) async {
switch changeType {
case .signIn: break
case .signOut, .switchAccounts:
isDeleteLocalDataAlertPresented = true
@unknown default: break
}
}
}
Scene Delegate for Sharing
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@Dependency(\.defaultSyncEngine) var syncEngine
func windowScene(
_ windowScene: UIWindowScene,
userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata
) {
Task {
try await syncEngine.acceptShare(metadata: cloudKitShareMetadata)
}
}
}
Manual Sync
@Dependency(\.defaultSyncEngine) var syncEngine
// Pull changes:
try await syncEngine.syncChanges()
// Delete local data:
try await syncEngine.deleteLocalData()
Table Definitions for CloudKit Sync
Use ON CONFLICT REPLACE DEFAULT for columns to handle sync conflicts:
try #sql("""
CREATE TABLE "counters" (
"id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()),
"count" INT NOT NULL ON CONFLICT REPLACE DEFAULT 0
) STRICT
""")
.execute(db)
9. Testing & Previews
Xcode Previews
#Preview {
let _ = prepareDependencies {
$0.defaultDatabase = try! appDatabase()
}
NavigationStack {
ContentView()
}
}
With Seed Data
#if DEBUG
extension DatabaseWriter {
func seedSampleData() throws {
try write { db in
try db.seed {
Item(id: UUID(), title: "Sample 1", isInStock: true, notes: "")
Item(id: UUID(), title: "Sample 2", isInStock: false, notes: "Note")
}
}
}
}
#endif
#Preview {
let _ = try! prepareDependencies {
try $0.bootstrapDatabase()
try? $0.defaultDatabase.seedSampleData()
}
NavigationStack {
ContentView()
}
}
In-Memory Database for Testing
extension DatabaseWriter where Self == DatabaseQueue {
static var testDatabase: Self {
let databaseQueue = try! DatabaseQueue()
var migrator = DatabaseMigrator()
migrator.registerMigration("Create tables") { db in
try #sql("""
CREATE TABLE "facts" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"body" TEXT NOT NULL
) STRICT
""")
.execute(db)
}
try! migrator.migrate(databaseQueue)
return databaseQueue
}
}
Unit Tests
@Test(.dependency(\.defaultDatabase, try appDatabase()))
func feature() {
// ...
}
10. Complete Examples
Minimal SwiftUI App (SwiftData Template Replacement)
import SQLiteData
import SwiftUI
@main
struct MyApp: App {
init() {
prepareDependencies {
$0.defaultDatabase = .appDatabase
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@FetchAll(Item.all, animation: .default) private var items
@Dependency(\.defaultDatabase) private var database
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) { EditButton() }
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
private func addItem() {
withErrorReporting {
try database.write { db in
try Item.insert().execute(db)
}
}
}
private func deleteItems(offsets: IndexSet) {
withErrorReporting {
try database.write { db in
try Item.where { $0.id.in(offsets.map { items[$0].id }) }.delete().execute(db)
}
}
}
}
@Table
nonisolated struct Item: Identifiable {
let id: Int
var timestamp: Date
}
extension DatabaseWriter where Self == DatabaseQueue {
static var appDatabase: Self {
let databaseQueue = try! DatabaseQueue()
var migrator = DatabaseMigrator()
migrator.registerMigration("Create items table") { db in
try #sql("""
CREATE TABLE "items" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"timestamp" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT
""")
.execute(db)
}
try! migrator.migrate(databaseQueue)
return databaseQueue
}
}
@Observable Model Pattern
@Observable
@MainActor
class FeatureModel {
@ObservationIgnored
@FetchAll(Item.order { $0.id.desc() }, animation: .default)
var items
@ObservationIgnored
@FetchOne(Item.count(), animation: .default)
var itemsCount = 0
@ObservationIgnored
@Dependency(\.defaultDatabase) var database
func addItem(body: String) async {
await withErrorReporting {
try await database.write { db in
try Item.insert { Item.Draft(body: body) }.execute(db)
}
}
}
func deleteItems(indices: IndexSet) {
withErrorReporting {
try database.write { db in
let ids = indices.map { items[$0].id }
try Item.where { $0.id.in(ids) }.delete().execute(db)
}
}
}
}
UIKit View Controller Pattern
final class ItemsViewController: UICollectionViewController {
@FetchAll(Item.order { $0.id.desc() }, animation: .default)
private var items
@Dependency(\.defaultDatabase) var database
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
override func viewDidLoad() {
super.viewDidLoad()
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
cell, indexPath, item in
var configuration = cell.defaultContentConfiguration()
configuration.text = item.title
cell.contentConfiguration = configuration
}
dataSource = UICollectionViewDiffableDataSource<Section, Item>(
collectionView: collectionView
) { collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration, for: indexPath, item: item
)
}
// Observe database changes and update UI
observe { [weak self] in
guard let self else { return }
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.items])
snapshot.appendItems(items, toSection: .items)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
enum Section: Hashable { case items }
}
Database Triggers Pattern
try database.write { db in
// Auto-set position on insert
try Item.createTemporaryTrigger(
after: .insert { new in
Item
.find(new.id)
.update { $0.position = Item.select { ($0.position.max() ?? -1) + 1 } }
}
)
.execute(db)
// Cascade FTS updates on insert
try Reminder.createTemporaryTrigger(
after: .insert { new in
ReminderText.insert {
ReminderText.Columns(
rowid: new.rowid,
title: new.title,
notes: new.notes.replace("\\n", " "),
tags: ""
)
}
}
)
.execute(db)
// Cascade FTS updates on column change
try Reminder.createTemporaryTrigger(
after: .update {
($0.title, $0.notes)
} forEachRow: { _, new in
ReminderText
.where { $0.rowid.eq(new.rowid) }
.update {
$0.title = new.title
$0.notes = new.notes.replace("\\n", " ")
}
}
)
.execute(db)
// Cascade FTS delete
try Reminder.createTemporaryTrigger(
after: .delete { old in
ReminderText
.where { $0.rowid.eq(old.rowid) }
.delete()
}
)
.execute(db)
}
Quick Reference
| SwiftData | SQLiteData |
|---|---|
@Model class | @Table struct |
@Query var items: [Item] | @FetchAll var items: [Item] |
@Query(sort:) | @FetchAll(Item.order(by:)) |
@Query(filter:) | @FetchAll(Item.where(...)) |
| N/A | @FetchOne(Item.count()) |
| N/A | @Fetch(CustomRequest()) |
@Environment(\.modelContext) | @Dependency(\.defaultDatabase) |
modelContext.insert(item) | try Item.insert { item }.execute(db) |
try modelContext.save() | (auto-saved within database.write) |
modelContext.delete(item) | try Item.delete(item).execute(db) |
ModelContainer(...) | prepareDependencies { $0.defaultDatabase = ... } |
| iOS 17+ only | iOS 13+ supported |
Views only (@Query) | Views, @Observable, UIKit, anywhere |
Platform Support
SQLiteData supports: iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+ — much broader than SwiftData's iOS 17+ requirement.