Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: xcodebuild ${{ matrix.scheme }}
run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="${{ matrix.scheme }}" xcodebuild-raw
- name: Output test failures
run: |
xcrun xcresulttool get test-results tests --path TestResults.xcresult --format json | jq -r '
.testNodes[]
| .. | objects | select(.nodeType=="Test Case")
| . as $tc
| ($tc.children // [])[]
| select(.nodeType=="Failure Message")
| "✘ Test \($tc.name) recorded an issue at \(.name)\n"
'
if: failure()

# NB: GRDB 7.6.1 does not currently build on Linux.
# linux:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ DerivedData/
.swiftpm
.netrc
*.sqlite
*.xcresult
43 changes: 26 additions & 17 deletions Examples/Reminders/ReminderRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ struct ReminderRow: View {
let reminder: Reminder
let remindersList: RemindersList
let showCompleted: Bool
let tags: [String]
let tags: String
let title: String?

@State var editReminder: Reminder.Draft?
@State var isCompleted: Bool
Expand All @@ -22,7 +23,8 @@ struct ReminderRow: View {
reminder: Reminder,
remindersList: RemindersList,
showCompleted: Bool,
tags: [String]
tags: String,
title: String? = nil
) {
self.color = color
self.isPastDue = isPastDue
Expand All @@ -31,6 +33,7 @@ struct ReminderRow: View {
self.remindersList = remindersList
self.showCompleted = showCompleted
self.tags = tags
self.title = title
self.isCompleted = reminder.isCompleted
}

Expand All @@ -44,10 +47,10 @@ struct ReminderRow: View {
.padding([.trailing], 5)
}
VStack(alignment: .leading) {
title(for: reminder)
title(for: reminder, title: title)

if !notes.isEmpty {
Text(notes)
highlight(notes)
.font(.subheadline)
.foregroundStyle(.gray)
.lineLimit(2)
Expand Down Expand Up @@ -145,28 +148,34 @@ struct ReminderRow: View {
}

private var subtitleText: Text {
let tagsText = tags.reduce(Text(reminder.dueDate == nil ? "" : " ")) { result, tag in
result + Text("#\(tag) ")
}
return
(dueText
+ tagsText
.foregroundStyle(.gray)
.bold())
.font(.callout)
Text(
"""
\(dueText)\(reminder.dueDate == nil ? "" : " ")\(highlight(tags).foregroundStyle(.gray))
"""
)
.font(.callout)
}

private func title(for reminder: Reminder) -> some View {
return HStack(alignment: .firstTextBaseline) {
@ViewBuilder
private func title(for reminder: Reminder, title: String?) -> some View {
HStack(alignment: .firstTextBaseline) {
if let priority = reminder.priority {
Text(String(repeating: "!", count: priority.rawValue))
.foregroundStyle(isCompleted ? .gray : remindersList.color)
}
Text(reminder.title)
highlight(title ?? reminder.title)
.foregroundStyle(isCompleted ? .gray : .primary)
}
.font(.title3)
}

func highlight(_ text: String) -> Text {
if let attributedText = try? AttributedString(markdown: text) {
Text(attributedText)
} else {
Text(text)
}
}
}

struct ReminderRowPreview: PreviewProvider {
Expand All @@ -190,7 +199,7 @@ struct ReminderRowPreview: PreviewProvider {
reminder: reminder,
remindersList: remindersList,
showCompleted: true,
tags: ["point-free", "adulting"]
tags: "#point-free #adulting"
)
}
}
Expand Down
18 changes: 8 additions & 10 deletions Examples/Reminders/RemindersDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class RemindersDetailModel: HashableObject {
let ids = Array(ids.enumerated())
let (first, rest) = (ids.first!, ids.dropFirst())
$0.position =
rest
rest
.reduce(Case($0.id).when(first.element, then: first.offset)) { cases, id in
cases.when(id.element, then: id.offset)
}
Expand All @@ -58,22 +58,21 @@ class RemindersDetailModel: HashableObject {
$ordering.withLock { $0 = .manual }
await updateQuery()
}

private func updateQuery() async {
await withErrorReporting {
try await $reminderRows.load(remindersQuery, animation: .default)
}
}

private var remindersQuery: some StructuredQueriesCore.Statement<Row> {
let query =
Reminder
.where {
if !showCompleted {
!$0.isCompleted
}
}
.order { $0.isCompleted }
.order(by: \.isCompleted)
.order {
switch ordering {
case .dueDate: $0.dueDate.asc(nulls: .last)
Expand All @@ -95,16 +94,16 @@ class RemindersDetailModel: HashableObject {
}
}
.join(RemindersList.all) { $0.remindersListID.eq($3.id) }
.join(ReminderText.all) { $0.rowid.eq($4.rowid) }
.select {
Row.Columns(
reminder: $0,
remindersList: $3,
isPastDue: $0.isPastDue,
notes: $0.inlineNotes.substr(0, 200),
tags: #sql("\($2.jsonTitles)")
notes: $4.notes.substr(0, 200),
tags: $4.tags
)
}
return query
}

enum Ordering: String, CaseIterable {
Expand Down Expand Up @@ -141,8 +140,7 @@ class RemindersDetailModel: HashableObject {
let remindersList: RemindersList
let isPastDue: Bool
let notes: String
@Column(as: [String].JSONRepresentation.self)
let tags: [String]
let tags: String
}
}

Expand Down Expand Up @@ -191,7 +189,7 @@ struct RemindersDetailView: View {
reminder: Reminder.Draft(remindersListID: remindersList.id),
remindersList: remindersList
)
.navigationTitle("New Reminder")
.navigationTitle("New Reminder")
}
}
}
Expand Down
18 changes: 14 additions & 4 deletions Examples/Reminders/RemindersLists.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ struct RemindersListsView: View {

var body: some View {
List {
if model.searchRemindersModel.searchText.isEmpty {
if model.searchRemindersModel.isSearching {
SearchRemindersView(model: model.searchRemindersModel)
} else {
Section {
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 16) {
GridRow {
Expand Down Expand Up @@ -269,8 +271,6 @@ struct RemindersListsView: View {
.padding([.leading, .trailing], 4)
}
.listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12))
} else {
SearchRemindersView(model: model.searchRemindersModel)
}
}
.onAppear {
Expand Down Expand Up @@ -328,7 +328,17 @@ struct RemindersListsView: View {
}
.presentationDetents([.medium])
}
.searchable(text: $model.searchRemindersModel.searchText)
.searchable(
text: $model.searchRemindersModel.searchText,
tokens: $model.searchRemindersModel.searchTokens
) { token in
switch token.kind {
case .near:
Text(token.rawValue)
case .tag:
Text("#\(token.rawValue)")
}
}
.navigationDestination(item: $model.destination.detail) { detailModel in
RemindersDetailView(model: detailModel)
}
Expand Down
92 changes: 77 additions & 15 deletions Examples/Reminders/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ enum Priority: Int, Codable, QueryBindable {

extension Reminder {
static let incomplete = Self.where { !$0.isCompleted }
static func searching(_ text: String) -> Where<Reminder> {
Self.where {
$0.title.collate(.nocase).contains(text)
|| $0.notes.collate(.nocase).contains(text)
}
}
static let withTags = group(by: \.id)
.leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) }
.leftJoin(Tag.all) { $1.tagID.eq($2.primaryKey) }
Expand All @@ -72,9 +66,6 @@ extension Reminder.TableColumns {
var isScheduled: some QueryExpression<Bool> {
!isCompleted && dueDate.isNot(nil)
}
var inlineNotes: some QueryExpression<String> {
notes.replace("\n", " ")
}
}

extension Tag {
Expand All @@ -83,19 +74,21 @@ extension Tag {
.leftJoin(Reminder.all) { $1.reminderID.eq($2.id) }
}

extension Tag.TableColumns {
var jsonTitles: some QueryExpression<[String].JSONRepresentation> {
self.title.jsonGroupArray(filter: self.title.isNot(nil))
}
}

@Table("remindersTags")
struct ReminderTag: Hashable, Identifiable {
let id: UUID
var reminderID: Reminder.ID
var tagID: Tag.ID
}

@Table @Selection
struct ReminderText: StructuredQueries.FTS5 {
let rowid: Int
let title: String
let notes: String
let tags: String
}

func appDatabase() throws -> any DatabaseWriter {
@Dependency(\.context) var context
let database: any DatabaseWriter
Expand Down Expand Up @@ -173,6 +166,18 @@ func appDatabase() throws -> any DatabaseWriter {
"""
)
.execute(db)
try #sql(
"""
CREATE VIRTUAL TABLE "reminderTexts" USING fts5(
"reminderID" UNINDEXED,
"title",
"notes",
"tags",
tokenize = 'trigram'
)
"""
)
.execute(db)
}

try migrator.migrate(database)
Expand All @@ -188,12 +193,14 @@ func appDatabase() throws -> any DatabaseWriter {
.update { $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1} }
})
.execute(db)

try Reminder.createTemporaryTrigger(after: .insert { new in
Reminder
.find(new.id)
.update { $0.position = Reminder.select { ($0.position.max() ?? -1) + 1} }
})
.execute(db)

try RemindersList.createTemporaryTrigger(after: .delete { _ in
RemindersList.insert {
RemindersList.Draft(
Expand All @@ -205,6 +212,61 @@ func appDatabase() throws -> any DatabaseWriter {
!RemindersList.exists()
})
.execute(db)

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)

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)

try Reminder.createTemporaryTrigger(after: .delete { old in
ReminderText
.where { $0.rowid.eq(old.rowid) }
.delete()
})
.execute(db)

func updateReminderTextTags(
for reminderID: some QueryExpression<Reminder.ID>
) -> UpdateOf<ReminderText> {
ReminderText
.where { $0.rowid.eq(Reminder.find(reminderID).select(\.rowid)) }
.update {
$0.tags = ReminderTag
.order(by: \.tagID)
.where { $0.reminderID.eq(reminderID) }
.join(Tag.all) { $0.tagID.eq($1.primaryKey) }
.select { ("#" + $1.title).groupConcat(" ") ?? "" }
}
}

try ReminderTag.createTemporaryTrigger(after: .insert { new in
updateReminderTextTags(for: new.reminderID)
})
.execute(db)

try ReminderTag.createTemporaryTrigger(after: .delete { old in
updateReminderTextTags(for: old.reminderID)
})
.execute(db)
}

return database
Expand Down
Loading