Library design: cards + redesigned list on the bookmarks screen, tag picker sheet #9
@@ -25,6 +25,7 @@
|
|||||||
457FCE503CCA82C5F27C6C90 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA428181B35885F7D9F4D55 /* Bookmark.swift */; };
|
457FCE503CCA82C5F27C6C90 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA428181B35885F7D9F4D55 /* Bookmark.swift */; };
|
||||||
50F3BED92EBA34F863C9F8A0 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7510EB352E624C7C9656EA33 /* LibraryView.swift */; };
|
50F3BED92EBA34F863C9F8A0 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7510EB352E624C7C9656EA33 /* LibraryView.swift */; };
|
||||||
55CDFFAB5530D08861F85363 /* LibraryGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF2B010DADCCFBC282D37F0 /* LibraryGridView.swift */; };
|
55CDFFAB5530D08861F85363 /* LibraryGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF2B010DADCCFBC282D37F0 /* LibraryGridView.swift */; };
|
||||||
|
59E46C9653C9033F5BB1CEC7 /* SpotlightRetrievalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */; };
|
||||||
5D86F3F0F603B248776916C7 /* BookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFB5EFC9764B22A2622EA4A /* BookmarksViewModel.swift */; };
|
5D86F3F0F603B248776916C7 /* BookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFB5EFC9764B22A2622EA4A /* BookmarksViewModel.swift */; };
|
||||||
5ED7F0AB24549BA01757A39C /* PodcastPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4EB8C63735A267B81030CB5 /* PodcastPlayerView.swift */; };
|
5ED7F0AB24549BA01757A39C /* PodcastPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4EB8C63735A267B81030CB5 /* PodcastPlayerView.swift */; };
|
||||||
66D5D90A5FAF842BCA0FE72D /* PodcastRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27A97922BAEBDC9C5A7385C /* PodcastRequests.swift */; };
|
66D5D90A5FAF842BCA0FE72D /* PodcastRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27A97922BAEBDC9C5A7385C /* PodcastRequests.swift */; };
|
||||||
@@ -136,6 +137,7 @@
|
|||||||
22E006A11D594BFC00A9C4B4 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
22E006A11D594BFC00A9C4B4 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||||
23F172EC9977CD5C51B228B9 /* MarksWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = MarksWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
23F172EC9977CD5C51B228B9 /* MarksWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = MarksWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
240DBB87940F8D255A812EB2 /* BookmarkActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkActions.swift; sourceTree = "<group>"; };
|
240DBB87940F8D255A812EB2 /* BookmarkActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkActions.swift; sourceTree = "<group>"; };
|
||||||
|
2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightRetrievalTests.swift; sourceTree = "<group>"; };
|
||||||
41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightBookmarkSearch.swift; sourceTree = "<group>"; };
|
41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightBookmarkSearch.swift; sourceTree = "<group>"; };
|
||||||
47CB3AAED5B64809B06A9650 /* RecentPodcastsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentPodcastsWidget.swift; sourceTree = "<group>"; };
|
47CB3AAED5B64809B06A9650 /* RecentPodcastsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentPodcastsWidget.swift; sourceTree = "<group>"; };
|
||||||
49685B8F3FEC72E8CF75843E /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
49685B8F3FEC72E8CF75843E /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||||
@@ -244,6 +246,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
7623601C25E481DF58371F2A /* AppIntentsTests.swift */,
|
7623601C25E481DF58371F2A /* AppIntentsTests.swift */,
|
||||||
|
2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */,
|
||||||
);
|
);
|
||||||
path = MarksTests;
|
path = MarksTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -528,6 +531,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
FD656A44CEE8AE28B136AD85 /* AppIntentsTests.swift in Sources */,
|
FD656A44CEE8AE28B136AD85 /* AppIntentsTests.swift in Sources */,
|
||||||
|
59E46C9653C9033F5BB1CEC7 /* SpotlightRetrievalTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,17 @@ struct BookmarkEntity: AppEntity, IndexedEntity {
|
|||||||
@Property(title: "Title")
|
@Property(title: "Title")
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
@Property(title: "URL")
|
/// The explicit `indexingKey` is load-bearing. Left to itself App Intents
|
||||||
|
/// indexes this property under the attribute set's own `url` key, and
|
||||||
|
/// Spotlight's Cascade translator types that field as NSString — so every
|
||||||
|
/// item failed with "Provided object for field url is of class NSURL,
|
||||||
|
/// expected class: NSString" and nothing reached the index. `contentURL`
|
||||||
|
/// is URL-typed there, and is the right field for "where this lives"
|
||||||
|
/// anyway. Routing it there keeps the property a URL for Shortcuts.
|
||||||
|
///
|
||||||
|
/// The failure is silent: translation happens after `indexAppEntities`
|
||||||
|
/// returns, so indexing logs success either way.
|
||||||
|
@Property(title: "URL", indexingKey: \.contentURL)
|
||||||
var url: URL
|
var url: URL
|
||||||
|
|
||||||
@Property(title: "Website")
|
@Property(title: "Website")
|
||||||
@@ -53,7 +63,7 @@ struct BookmarkEntity: AppEntity, IndexedEntity {
|
|||||||
attrs.title = title
|
attrs.title = title
|
||||||
attrs.contentDescription = details.isEmpty ? summary : details
|
attrs.contentDescription = details.isEmpty ? summary : details
|
||||||
attrs.keywords = tags
|
attrs.keywords = tags
|
||||||
attrs.url = url
|
attrs.contentURL = url
|
||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ enum SpotlightBookmarkSearch {
|
|||||||
Log.spotlight.debug("Search query=\(rawQuery, privacy: .public) predicate=\(queryString, privacy: .public)")
|
Log.spotlight.debug("Search query=\(rawQuery, privacy: .public) predicate=\(queryString, privacy: .public)")
|
||||||
|
|
||||||
let context = CSSearchQueryContext()
|
let context = CSSearchQueryContext()
|
||||||
context.fetchAttributes = ["title", "contentDescription", "keywords", "url"]
|
context.fetchAttributes = ["title", "contentDescription", "keywords", "contentURL"]
|
||||||
let query = CSSearchQuery(queryString: queryString, queryContext: context)
|
let query = CSSearchQuery(queryString: queryString, queryContext: context)
|
||||||
|
|
||||||
var out: [RetrievedBookmark] = []
|
var out: [RetrievedBookmark] = []
|
||||||
@@ -33,9 +33,9 @@ enum SpotlightBookmarkSearch {
|
|||||||
let a = result.item.attributeSet
|
let a = result.item.attributeSet
|
||||||
out.append(RetrievedBookmark(
|
out.append(RetrievedBookmark(
|
||||||
title: a.title ?? "Untitled",
|
title: a.title ?? "Untitled",
|
||||||
host: a.url?.host() ?? "",
|
host: a.contentURL?.host() ?? "",
|
||||||
description: a.contentDescription ?? "",
|
description: a.contentDescription ?? "",
|
||||||
url: a.url?.absoluteString ?? ""
|
url: a.contentURL?.absoluteString ?? ""
|
||||||
))
|
))
|
||||||
if out.count >= limit { break }
|
if out.count >= limit { break }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import Testing
|
||||||
|
import CoreSpotlight
|
||||||
|
@testable import Marks
|
||||||
|
|
||||||
|
/// End-to-end guard for the whole Spotlight path: index a bookmark, then
|
||||||
|
/// retrieve it the way the on-device assistant does.
|
||||||
|
///
|
||||||
|
/// Worth keeping. The bug this was written for made every item fail to
|
||||||
|
/// translate into the index while `indexAppEntities` still reported success,
|
||||||
|
/// so nothing short of a round trip would have caught it.
|
||||||
|
struct SpotlightRetrievalTests {
|
||||||
|
@Test func indexedBookmarkIsRetrievable() async throws {
|
||||||
|
let b = Bookmark(
|
||||||
|
id: 999_001,
|
||||||
|
url: "https://github.com/example/zqxjkltest",
|
||||||
|
title: "Zqxjkltest concurrency notes",
|
||||||
|
description: "A distinctive probe document about zqxjkltest.",
|
||||||
|
tagNames: ["zqxjkltest"],
|
||||||
|
dateAdded: Date(), dateModified: Date(),
|
||||||
|
isArchived: false, unread: false, shared: false,
|
||||||
|
websiteTitle: nil, websiteDescription: nil,
|
||||||
|
faviconUrl: nil, previewImageUrl: nil,
|
||||||
|
aiSummary: nil, aiTags: nil
|
||||||
|
)
|
||||||
|
await SpotlightIndexer.index([b])
|
||||||
|
var hits: [RetrievedBookmark] = []
|
||||||
|
for _ in 0..<20 {
|
||||||
|
try? await Task.sleep(for: .milliseconds(700))
|
||||||
|
hits = await SpotlightBookmarkSearch.run(query: "zqxjkltest", limit: 5)
|
||||||
|
if !hits.isEmpty { break }
|
||||||
|
}
|
||||||
|
await SpotlightIndexer.remove(ids: [999_001])
|
||||||
|
#expect(!hits.isEmpty, "Spotlight returned no hits for an indexed bookmark")
|
||||||
|
let hit = try #require(hits.first)
|
||||||
|
#expect(hit.title.contains("Zqxjkltest"))
|
||||||
|
#expect(hit.url == "https://github.com/example/zqxjkltest",
|
||||||
|
"url did not survive the round trip: \(hit.url)")
|
||||||
|
#expect(hit.host == "github.com", "host was \(hit.host)")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user