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..85f6ad74 --- /dev/null +++ b/Examples/CaseStudies/SectionedQuery.swift @@ -0,0 +1,131 @@ +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. + + Use the picker to change how the reminders are sectioned: by category, by priority, or with \ + 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.none) 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 sectioning != .none { + Text(section.name ?? "None") + } + } + } + } + .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/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() { 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 */