From eaa6b30e9e2264fa19d2b5e7598a01ae7aadd7c5 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 19 Aug 2026 10:44:37 -0500 Subject: [PATCH 1/3] Use sectionBy in CloudKit demo. --- .../CloudKitDemo/CountersListFeature.swift | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/Examples/CloudKitDemo/CountersListFeature.swift b/Examples/CloudKitDemo/CountersListFeature.swift index 72bdfd9a..6e16e832 100644 --- a/Examples/CloudKitDemo/CountersListFeature.swift +++ b/Examples/CloudKitDemo/CountersListFeature.swift @@ -8,8 +8,18 @@ struct CountersListView: View { .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($1.id) } .select { Row.Columns(counter: $0, isShared: $1.isShared.ifnull(false)) - } - ) var rows + }, + sectionBy: { _, metadata in + Case() + .when(metadata.isShared.eq(true), then: "Shared") + .else("Private") + .desc() + } + ) + var rows + + @State var sharedRecord: SharedRecord? + @Dependency(\.defaultDatabase) var database @Dependency(\.defaultSyncEngine) var syncEngine @@ -20,10 +30,12 @@ struct CountersListView: View { var body: some View { List { - if !rows.isEmpty { - Section { - ForEach(rows, id: \.counter.id) { row in - CounterRow(row: row) + ForEach($rows.sections) { section in + Section(section.name ?? "Private") { + ForEach(section, id: \.counter.id) { row in + CounterRow(row: row) { + shareButtonTapped(row: row) + } .buttonStyle(.borderless) } .onDelete { indexSet in @@ -49,6 +61,9 @@ struct CountersListView: View { } } } + .sheet(item: $sharedRecord) { sharedRecord in + CloudSharingView(sharedRecord: sharedRecord) + } } func deleteRows(at indexSet: IndexSet) { @@ -61,11 +76,19 @@ struct CountersListView: View { } } } + + func shareButtonTapped(row: Row) { + _ = Task { + sharedRecord = try await syncEngine.share(record: row.counter) { share in + share[CKShare.SystemFieldKey.title] = "Join my counter!" + } + } + } } struct CounterRow: View { let row: CountersListView.Row - @State var sharedRecord: SharedRecord? + let onShare: () -> Void @Dependency(\.defaultDatabase) var database @Dependency(\.defaultSyncEngine) var syncEngine @@ -84,23 +107,12 @@ struct CounterRow: View { } Spacer() Button { - shareButtonTapped() + onShare() } label: { Image(systemName: "square.and.arrow.up") } } } - .sheet(item: $sharedRecord) { sharedRecord in - CloudSharingView(sharedRecord: sharedRecord) - } - } - - func shareButtonTapped() { - _ = Task { - sharedRecord = try await syncEngine.share(record: row.counter) { share in - share[CKShare.SystemFieldKey.title] = "Join my counter!" - } - } } func decrementButtonTapped() { From 820bdadf05757e48e61c20dee713ff35ad190112 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 19 Aug 2026 12:07:09 -0500 Subject: [PATCH 2/3] wip --- Examples/CaseStudies/README.md | 2 + Examples/CaseStudies/SectionedQuery.swift | 138 ++++++++++++++++++++ Examples/Examples.xcodeproj/project.pbxproj | 2 +- 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 Examples/CaseStudies/SectionedQuery.swift diff --git a/Examples/CaseStudies/README.md b/Examples/CaseStudies/README.md index 4434a5b1..3a019034 100644 --- a/Examples/CaseStudies/README.md +++ b/Examples/CaseStudies/README.md @@ -9,6 +9,8 @@ SQLiteData, including: query, such as when searching for rows in a table that contains a fragment of text. * [@Observable Models](ObservableModelDemo.swift): Shows how to use the tools of this library in an `@Observable` model. + * [Sectioned Queries](SectionedQuery.swift): Shows how to group the results of a query into + sections using the `sectionBy:` argument of `@FetchAll`. * [SwiftUI](SwiftUIDemo.swift): Shows how to use the tools of this library directly in a SwiftUI view. * [Database Transactions](TransactionDemo.swift): Shows how to execute multiple queries within diff --git a/Examples/CaseStudies/SectionedQuery.swift b/Examples/CaseStudies/SectionedQuery.swift new file mode 100644 index 00000000..20bbc447 --- /dev/null +++ b/Examples/CaseStudies/SectionedQuery.swift @@ -0,0 +1,138 @@ +import SQLiteData +import SwiftUI + +struct SectionedQueryDemo: SwiftUICaseStudy { + let readMe = """ + This demonstrates how to group the results of a `@FetchAll` query into sections by providing \ + a `sectionBy:` argument. Results are ordered and grouped into a section for each distinct \ + value of the sectioning expression, and the sections are accessed from the property's \ + projected value. + + Use the picker to change how the reminders are sectioned: by category, by priority, or with \ + no sectioning at all. This is done by invoking the `load` method defined on the `@FetchAll` \ + projected value with a `sectionBy:` closure that can return a different expression depending \ + on state, or `nil` for no grouping. Reminders without a priority are grouped into a section \ + with a `nil` name. You can also delete rows by swiping on a row and tapping the "Delete" \ + button. + """ + let caseStudyTitle = "Sectioned Queries" + + @FetchAll(Reminder.order(by: \.title), sectionBy: \.category, animation: .default) + private var reminders + + @State private var sectioning = Sectioning.category + + @Dependency(\.defaultDatabase) var database + + private enum Sectioning: String, CaseIterable { + case none = "None" + case category = "Category" + case priority = "Priority" + } + + var body: some View { + List { + Section { + Picker("Section by", selection: $sectioning) { + ForEach(Sectioning.allCases, id: \.self) { sectioning in + Text(sectioning.rawValue) + } + } + .pickerStyle(.segmented) + } + ForEach($reminders.sections) { section in + Section { + ForEach(section) { reminder in + Text(reminder.title) + } + .onDelete { indices in + withErrorReporting { + try database.write { db in + let ids = indices.map { section[$0].id } + try Reminder + .where { $0.id.in(ids) } + .delete() + .execute(db) + } + } + } + } header: { + if let name = section.name { + Text(name) + } + } + } + } + .task(id: sectioning) { + await withErrorReporting { + try await $reminders.load( + Reminder.order(by: \.title), + sectionBy: { + switch sectioning { + case .none: + nil + case .category: + $0.category + case .priority: + $0.priority.asc(nulls: .last) + } + }, + animation: .default + ) + } + } + } +} + +@Table +nonisolated private struct Reminder: Identifiable { + let id: Int + var title: String + var category: String + var priority: String? +} + +extension DatabaseWriter where Self == DatabaseQueue { + static var sectionedQueryDatabase: Self { + let databaseQueue = try! DatabaseQueue() + var migrator = DatabaseMigrator() + migrator.registerMigration("Create 'reminders' table") { db in + try #sql( + """ + CREATE TABLE "reminders" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "category" TEXT NOT NULL, + "priority" TEXT + ) STRICT + """ + ) + .execute(db) + } + migrator.registerMigration("Seed 'reminders' table") { db in + try Reminder.insert { + Reminder.Draft(title: "Call mom", category: "Family", priority: "High") + Reminder.Draft(title: "Plan vacation", category: "Family") + Reminder.Draft(title: "Buy groceries", category: "Personal", priority: "High") + Reminder.Draft(title: "Go to the gym", category: "Personal", priority: "Low") + Reminder.Draft(title: "Pick up dry cleaning", category: "Personal") + Reminder.Draft(title: "Prepare talk", category: "Work", priority: "High") + Reminder.Draft(title: "Send status report", category: "Work", priority: "Low") + } + .execute(db) + } + try! migrator.migrate(databaseQueue) + return databaseQueue + } +} + +#Preview { + let _ = prepareDependencies { + $0.defaultDatabase = .sectionedQueryDatabase + } + NavigationStack { + CaseStudyView { + SectionedQueryDemo() + } + } +} diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 29b9f984..4d22126d 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 100; objects = { /* Begin PBXBuildFile section */ From 5d0157df04e35c5d1e85c7616da32da2b44cea1a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 19 Aug 2026 13:57:31 -0500 Subject: [PATCH 3/3] New case study for sectioning. --- Examples/CaseStudies/SectionedQuery.swift | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/Examples/CaseStudies/SectionedQuery.swift b/Examples/CaseStudies/SectionedQuery.swift index 20bbc447..85f6ad74 100644 --- a/Examples/CaseStudies/SectionedQuery.swift +++ b/Examples/CaseStudies/SectionedQuery.swift @@ -4,22 +4,15 @@ import SwiftUI struct SectionedQueryDemo: SwiftUICaseStudy { let readMe = """ This demonstrates how to group the results of a `@FetchAll` query into sections by providing \ - a `sectionBy:` argument. Results are ordered and grouped into a section for each distinct \ - value of the sectioning expression, and the sections are accessed from the property's \ - projected value. + a `sectionBy:` argument. Use the picker to change how the reminders are sectioned: by category, by priority, or with \ - no sectioning at all. This is done by invoking the `load` method defined on the `@FetchAll` \ - projected value with a `sectionBy:` closure that can return a different expression depending \ - on state, or `nil` for no grouping. Reminders without a priority are grouped into a section \ - with a `nil` name. You can also delete rows by swiping on a row and tapping the "Delete" \ - button. + no sectioning at all. When grouping by priority, the section of reminders with no priority \ + are ordered to the bottom of the list. """ let caseStudyTitle = "Sectioned Queries" - @FetchAll(Reminder.order(by: \.title), sectionBy: \.category, animation: .default) - private var reminders - + @FetchAll(Reminder.none) private var reminders @State private var sectioning = Sectioning.category @Dependency(\.defaultDatabase) var database @@ -57,15 +50,15 @@ struct SectionedQueryDemo: SwiftUICaseStudy { } } } header: { - if let name = section.name { - Text(name) + if sectioning != .none { + Text(section.name ?? "None") } } } } .task(id: sectioning) { await withErrorReporting { - try await $reminders.load( + _ = try await $reminders.load( Reminder.order(by: \.title), sectionBy: { switch sectioning {