diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c326768b..3e56ff26 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5bbd661afed2838bb182df109e97091f7e27810d425e696e92232fba72efe73f", + "originHash" : "8b698458b719345f562ad056ad435aa3db5d6e852f96e6ca49566c5d31ffb528", "pins" : [ { "identity" : "combine-schedulers", diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 8a88abde..b7b70ed3 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -91,11 +91,11 @@ struct ReminderFormView: View { } } Picker(selection: $reminder.priority) { - Text("None").tag(Priority?.none) + Text("None").tag(Reminder.Priority?.none) Divider() - Text("High").tag(Priority.high) - Text("Medium").tag(Priority.medium) - Text("Low").tag(Priority.low) + Text("High").tag(Reminder.Priority.high) + Text("Medium").tag(Reminder.Priority.medium) + Text("Low").tag(Reminder.Priority.low) } label: { HStack { Image(systemName: "exclamationmark.circle.fill") diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index c67f50bc..efe509ad 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -12,7 +12,6 @@ struct ReminderRow: View { let title: String? @State var editReminder: Reminder.Draft? - @State var isCompleted: Bool @Dependency(\.defaultDatabase) private var database @@ -34,14 +33,13 @@ struct ReminderRow: View { self.showCompleted = showCompleted self.tags = tags self.title = title - self.isCompleted = reminder.isCompleted } var body: some View { HStack { HStack(alignment: .firstTextBaseline) { Button(action: completeButtonTapped) { - Image(systemName: isCompleted ? "circle.inset.filled" : "circle") + Image(systemName: reminder.isCompleted ? "circle.inset.filled" : "circle") .foregroundStyle(.gray) .font(.title2) .padding([.trailing], 5) @@ -59,7 +57,7 @@ struct ReminderRow: View { } } Spacer() - if !isCompleted { + if !reminder.isCompleted { HStack { if reminder.isFlagged { Image(systemName: "flag.fill") @@ -104,36 +102,15 @@ struct ReminderRow: View { .navigationTitle("Details") } } - .task(id: isCompleted) { - guard !showCompleted else { return } - guard - isCompleted, - isCompleted != reminder.isCompleted - else { return } - do { - try await Task.sleep(for: .seconds(2)) - toggleCompletion() - } catch {} - } } private func completeButtonTapped() { - if showCompleted { - toggleCompletion() - } else { - isCompleted.toggle() - } - } - - private func toggleCompletion() { withErrorReporting { try database.write { db in - isCompleted = - try Reminder + try Reminder .find(reminder.id) - .update { $0.isCompleted.toggle() } - .returning(\.isCompleted) - .fetchOne(db) ?? isCompleted + .update { $0.toggleStatus() } + .execute(db) } } } @@ -161,10 +138,10 @@ struct ReminderRow: View { HStack(alignment: .firstTextBaseline) { if let priority = reminder.priority { Text(String(repeating: "!", count: priority.rawValue)) - .foregroundStyle(isCompleted ? .gray : remindersList.color) + .foregroundStyle(reminder.isCompleted ? .gray : remindersList.color) } highlight(title ?? reminder.title) - .foregroundStyle(isCompleted ? .gray : .primary) + .foregroundStyle(reminder.isCompleted ? .gray : .primary) } .font(.title3) } diff --git a/Examples/Reminders/RemindersDetail.swift b/Examples/Reminders/RemindersDetail.swift index afc42e10..aee49d67 100644 --- a/Examples/Reminders/RemindersDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -92,10 +92,16 @@ class RemindersDetailModel: HashableObject { Reminder .where { if !showCompleted { - !$0.isCompleted + $0.status.neq(Reminder.Status.completed) + } + } + .order { + if showCompleted { + $0.isCompleted + } else { + $0.status.eq(Reminder.Status.completed) } } - .order(by: \.isCompleted) .order { switch ordering { case .dueDate: $0.dueDate.asc(nulls: .last) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 039fa024..27ea9981 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -4,6 +4,7 @@ import IssueReporting import OSLog import SQLiteData import SwiftUI +import Synchronization @Table struct RemindersList: Hashable, Identifiable { @@ -31,13 +32,33 @@ struct RemindersListAsset: Hashable, Identifiable { struct Reminder: Hashable, Identifiable { let id: UUID var dueDate: Date? - var isCompleted = false var isFlagged = false var notes = "" var position = 0 var priority: Priority? var remindersListID: RemindersList.ID + var status: Status = .incomplete var title = "" + var isCompleted: Bool { + status != .incomplete + } + enum Priority: Int, QueryBindable { + case low = 1 + case medium + case high + } + enum Status: Int, QueryBindable { + case completed = 1 + case completing = 2 + case incomplete = 0 + } +} +extension Updates { + mutating func toggleStatus() { + self.status = Case(self.status) + .when(Reminder.Status.incomplete, then: Reminder.Status.completing) + .else(Reminder.Status.incomplete) + } } extension Reminder.Draft: Identifiable {} @@ -49,12 +70,6 @@ struct Tag: Hashable, Identifiable { var id: String { title } } -enum Priority: Int, QueryBindable { - case low = 1 - case medium - case high -} - extension Reminder { static let incomplete = Self.where { !$0.isCompleted } static let withTags = group(by: \.id) @@ -63,6 +78,9 @@ extension Reminder { } extension Reminder.TableColumns { + var isCompleted: some QueryExpression { + status.neq(Reminder.Status.incomplete) + } var isPastDue: some QueryExpression { @Dependency(\.date.now) var now return !isCompleted && #sql("coalesce(date(\(dueDate)) < date(\(now)), 0)") @@ -117,6 +135,7 @@ func appDatabase() throws -> any DatabaseWriter { configuration.foreignKeysEnabled = true configuration.prepareDatabase { db in try db.attachMetadatabase() + db.add(function: $handleReminderStatusUpdate) #if DEBUG db.trace(options: .profile) { if context == .live { @@ -166,12 +185,12 @@ func appDatabase() throws -> any DatabaseWriter { CREATE TABLE "reminders" ( "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()), "dueDate" TEXT, - "isCompleted" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0, "isFlagged" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0, "notes" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT '', "position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0, "priority" INTEGER, "remindersListID" TEXT NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE, + "status" INTEGER NOT NULL DEFAULT 0, "title" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT '' ) STRICT """ @@ -313,6 +332,17 @@ func appDatabase() throws -> any DatabaseWriter { ) .execute(db) + try Reminder.createTemporaryTrigger( + after: .update { + $0.status + } forEachRow: { _, _ in + Values($handleReminderStatusUpdate()) + } when: { _, new in + new.status.eq(Reminder.Status.completing) + } + ) + .execute(db) + if context != .live { try db.seedSampleData() } @@ -321,6 +351,25 @@ func appDatabase() throws -> any DatabaseWriter { return database } +let reminderStatusMutex = Mutex?>(nil) +@DatabaseFunction +func handleReminderStatusUpdate() { + reminderStatusMutex.withLock { + $0?.cancel() + $0 = Task { + @Dependency(\.defaultDatabase) var database + @Dependency(\.continuousClock) var clock + try await clock.sleep(for: .seconds(5)) + try await database.write { db in + try Reminder + .where { $0.status.eq(Reminder.Status.completing) } + .update { $0.status = .completed } + .execute(db) + } + } + } +} + private let logger = Logger(subsystem: "Reminders", category: "Database") #if DEBUG @@ -370,8 +419,8 @@ private let logger = Logger(subsystem: "Reminders", category: "Database") Reminder( id: reminderIDs[3], dueDate: now.addingTimeInterval(-60 * 60 * 24 * 190), - isCompleted: true, remindersListID: remindersListIDs[0], + status: .completed, title: "Take a walk" ) Reminder( @@ -391,17 +440,17 @@ private let logger = Logger(subsystem: "Reminders", category: "Database") Reminder( id: reminderIDs[6], dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2), - isCompleted: true, priority: .low, remindersListID: remindersListIDs[1], + status: .completed, title: "Get laundry" ) Reminder( id: reminderIDs[7], dueDate: now.addingTimeInterval(60 * 60 * 24 * 4), - isCompleted: false, priority: .high, remindersListID: remindersListIDs[1], + status: .incomplete, title: "Take out trash" ) Reminder( @@ -418,16 +467,16 @@ private let logger = Logger(subsystem: "Reminders", category: "Database") Reminder( id: reminderIDs[9], dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2), - isCompleted: true, priority: .medium, remindersListID: remindersListIDs[2], + status: .completed, title: "Send weekly emails" ) Reminder( id: reminderIDs[10], dueDate: now.addingTimeInterval(60 * 60 * 24 * 2), - isCompleted: false, remindersListID: remindersListIDs[2], + status: .incomplete, title: "Prepare for WWDC" ) let tagIDs = ["car", "kids", "someday", "optional", "social", "night", "adulting"] diff --git a/Examples/RemindersTests/RemindersDetailsTests.swift b/Examples/RemindersTests/RemindersDetailsTests.swift index c4f9cd3d..5bec4b80 100644 --- a/Examples/RemindersTests/RemindersDetailsTests.swift +++ b/Examples/RemindersTests/RemindersDetailsTests.swift @@ -22,12 +22,12 @@ extension BaseTestSuite { reminder: Reminder( id: UUID(00000000-0000-0000-0000-000000000004), dueDate: Date(2009-02-11T23:31:30.000Z), - isCompleted: false, isFlagged: true, notes: "", position: 2, priority: nil, remindersListID: UUID(00000000-0000-0000-0000-000000000000), + status: .incomplete, title: "Haircut" ), remindersList: RemindersList( @@ -44,12 +44,12 @@ extension BaseTestSuite { reminder: Reminder( id: UUID(00000000-0000-0000-0000-000000000005), dueDate: Date(2009-02-13T23:31:30.000Z), - isCompleted: false, isFlagged: false, notes: "Ask about diet", position: 3, priority: .high, remindersListID: UUID(00000000-0000-0000-0000-000000000000), + status: .incomplete, title: "Doctor appointment" ), remindersList: RemindersList( @@ -66,12 +66,12 @@ extension BaseTestSuite { reminder: Reminder( id: UUID(00000000-0000-0000-0000-000000000007), dueDate: Date(2009-02-13T23:31:30.000Z), - isCompleted: false, isFlagged: false, notes: "", position: 5, priority: nil, remindersListID: UUID(00000000-0000-0000-0000-000000000000), + status: .incomplete, title: "Buy concert tickets" ), remindersList: RemindersList( @@ -88,7 +88,6 @@ extension BaseTestSuite { reminder: Reminder( id: UUID(00000000-0000-0000-0000-000000000003), dueDate: nil, - isCompleted: false, isFlagged: false, notes: """ Milk @@ -100,6 +99,7 @@ extension BaseTestSuite { position: 1, priority: nil, remindersListID: UUID(00000000-0000-0000-0000-000000000000), + status: .incomplete, title: "Groceries" ), remindersList: RemindersList(