SQLiteData Usage Guide

Comprehensive guide for using the SQLiteData library — a fast, lightweight replacement for SwiftData powered by SQL with CloudKit synchronization support.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "SQLiteData Usage Guide" with this command: npx skills add bitxeno/sqlite-data-skill/bitxeno-sqlite-data-skill-sqlitedata-usage-guide

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 @Table macro and type-safe query building APIs.
  • Swift Dependencies: Used for dependency injection (@Dependency, prepareDependencies).

Table of Contents

  1. Defining Your Schema
  2. Preparing the Database
  3. Fetching Data
  4. Observing Changes
  5. Dynamic Queries
  6. CRUD Operations
  7. Associations & Joins
  8. CloudKit Synchronization
  9. Testing & Previews
  10. 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 @ObservationIgnored due 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 @FetchAll to 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

SwiftDataSQLiteData
@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+ onlyiOS 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.

References

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

magister.net

Fetch schedule, grades, and infractions from https://magister.net 🇳🇱 portal

Registry SourceRecently Updated
1400ghuron
General

Official Doc

公文写作助手。通知、报告、请示、批复、会议纪要、工作总结、格式检查、语气检查、模板库。Official document writer for notices, reports, requests, meeting minutes with format check, tone check, template l...

Registry SourceRecently Updated
2392ckchzh
General

Douyin Creator

抖音内容创作与运营助手。抖音运营、抖音涨粉、短视频创作、抖音标题、抖音标签、抖音SEO、抖音账号运营、抖音数据分析、抖音选题、抖音脚本、抖音文案、抖音评论区运营、抖音人设定位、抖音发布时间、DOU+投放、抖音流量、短视频运营、视频创意、直播脚本、话题标签策略、合拍翻拍创意、抖音变现、带货星图、Douyin con...

Registry SourceRecently Updated
General

File Hasher

Compute, verify, and compare file hashes using MD5, SHA-1, SHA-256, SHA-512, and more. Use when checking file integrity, verifying downloads against expected...

Registry SourceRecently Updated