Skip to content

Commit 1d4a9a0

Browse files
authored
Fix SwiftData crash due to SwiftData missing relationships in certain conditions. (#1133)
fix(swiftData): replace relationships calls with direct fetches, due to a SwiftData bug that loses the relationship sometimes
1 parent 780a2ae commit 1d4a9a0

File tree

7 files changed

+140
-31
lines changed

7 files changed

+140
-31
lines changed

PocketKit/Sources/PocketKit/Home/Views/Card groups/SlateView.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ struct SlateView: View {
1111
let remoteID: String
1212
let slateTitle: String?
1313
let cards: [HomeCardConfiguration]
14-
let slateInfo: SlateInfo?
1514

1615
@Environment(\.layoutWidth)
1716
private var layoutWidth
@@ -38,7 +37,7 @@ struct SlateView: View {
3837
private extension SlateView {
3938
func makeHeader(_ title: String) -> some View {
4039
SectionHeader(title: title) {
41-
navigation.navigateTo(SlateDestination(slateID: remoteID, slateTitle: slateTitle, slateInfo: slateInfo))
40+
navigation.navigateTo(SlateDestination(slateID: remoteID, slateTitle: slateTitle))
4241
}
4342
}
4443

PocketKit/Sources/PocketKit/Home/Views/Detail views/SharedWithYouDetailView.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ struct SharedWithYouDetailView: View {
2020
@Environment(\.horizontalSizeClass)
2121
var horizontalSizeClass
2222

23+
@Environment(\.modelContext)
24+
private var modelContext
25+
2326
var body: some View {
2427
GeometryReader { proxy in
2528
CardCollection(cards: cards, size: .large, layoutWidth: layoutWidth(proxy.size))
@@ -39,7 +42,7 @@ struct SharedWithYouDetailView: View {
3942
private extension SharedWithYouDetailView {
4043
var proposedCards: [HomeCardConfiguration] {
4144
sharedWithYouItems.enumerated().compactMap {
42-
if let item = $0.element.item {
45+
if let item = fetchItem($0.element.url) {
4346
return HomeCardConfiguration(
4447
givenURL: item.givenURL,
4548
sharedWithYouUrlString: $0.element.url,
@@ -62,6 +65,19 @@ private extension SharedWithYouDetailView {
6265
return nil
6366
}
6467
}
68+
69+
/// Fetch an `Item` from the underlying `SharedWithYouItem`
70+
/// - Parameter sharedWithYouUrl: `SharedWithYouItem` url
71+
/// - Returns: the item, if it was found
72+
func fetchItem(_ sharedWithYouUrl: String) -> Item? {
73+
let predicate = #Predicate<Item> { $0.sharedWithYouItem?.url == sharedWithYouUrl }
74+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
75+
fetchDescriptor.fetchLimit = 1
76+
77+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
78+
return result.first
79+
}
80+
6581
/// Determine the size of the current layout
6682
/// **NOTE: turns out that, since this is a detail view, the environment value `layoutWidth`
6783
/// cannot be used here since the GeometryReader of HomeView is not active

PocketKit/Sources/PocketKit/Home/Views/Detail views/SlateDetailView.swift

+61-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ struct SlateDetailView: View {
1919
@Environment(\.homeActions)
2020
private var homeActions
2121

22+
@Environment(\.modelContext)
23+
private var modelContext
24+
2225
init(destination: SlateDestination) {
2326
self.destination = destination
2427
let slateID = destination.slateID
@@ -41,7 +44,7 @@ struct SlateDetailView: View {
4144
}
4245
}
4346
.onAppear {
44-
guard let slateInfo = destination.slateInfo else {
47+
guard let slateInfo = slateInfo(destination) else {
4548
return
4649
}
4750
homeActions.trackSlateDetailImpression(info: slateInfo)
@@ -56,7 +59,7 @@ struct SlateDetailView: View {
5659
private extension SlateDetailView {
5760
var proposedCards: [HomeCardConfiguration] {
5861
recommendations.enumerated().compactMap {
59-
if let item = $0.element.item {
62+
if let item = fetchItem($0.element.remoteID) {
6063
return HomeCardConfiguration(
6164
givenURL: item.givenURL,
6265
sharedWithYouUrlString: nil,
@@ -79,6 +82,62 @@ private extension SlateDetailView {
7982
return nil
8083
}
8184
}
85+
86+
/// Fetch analytics info for this slate
87+
/// - Parameter destination: slate destination of this slate
88+
/// - Returns: analytics info
89+
func slateInfo(_ destination: SlateDestination) -> SlateInfo? {
90+
guard let slate = fetchSlate(destination.slateID),
91+
let lineup = fetchSlateLineup() else {
92+
return nil
93+
}
94+
return SlateInfo(
95+
slateId: slate.remoteID,
96+
slateRequestId: slate.requestID,
97+
slateExperimentId: slate.experimentID,
98+
slateIndex: Int(slate.sortIndex ?? 0),
99+
slateLineupId: lineup.remoteID
100+
)
101+
}
102+
103+
/// Fetch an `Item` from the underlying `Recommendation`
104+
/// - Parameter recommendationID: `Recommendation` ID
105+
/// - Returns: the item, if it was found
106+
func fetchItem(_ recommendationID: String) -> Item? {
107+
let predicate = #Predicate<Item> { $0.recommendation?.remoteID == recommendationID }
108+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
109+
fetchDescriptor.fetchLimit = 1
110+
111+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
112+
return result.first
113+
}
114+
115+
/// Fetch the current slate from SwiftData
116+
/// - Parameter remoteID: the remote id of this slate
117+
/// - Returns: the slate, if it was found
118+
func fetchSlate(_ remoteID: String) -> Slate? {
119+
let predicate = #Predicate<Slate> { $0.remoteID == remoteID }
120+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
121+
fetchDescriptor.fetchLimit = 1
122+
123+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
124+
return result.first
125+
}
126+
127+
/// Fettch the current slate lineup
128+
/// - Returns: the slate lineup, if it was found
129+
func fetchSlateLineup() -> SlateLineup? {
130+
// there is only one lineup, so we don't need to filter this query
131+
let predicate = #Predicate<SlateLineup> { _ in
132+
return true
133+
}
134+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
135+
fetchDescriptor.fetchLimit = 1
136+
137+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
138+
return result.first
139+
}
140+
82141
/// Determine the size of the current layout
83142
/// **NOTE: turns out that, since this is a detail view, the environment value `layoutWidth`
84143
/// cannot be used here since the GeometryReader of HomeView is not active

PocketKit/Sources/PocketKit/Home/Views/Navigation/NavigationDestination.swift

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ protocol NavigationDestination: Codable, Hashable {}
1010
struct SlateDestination: NavigationDestination {
1111
let slateID: String
1212
let slateTitle: String?
13-
let slateInfo: SlateInfo?
1413
}
1514

1615
struct NativeCollectionDestination: NavigationDestination {

PocketKit/Sources/PocketKit/Home/Views/Top level views/RecentSavesView.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ struct RecentSavesView: View {
1515

1616
@State private var cards: [HomeCardConfiguration] = []
1717

18+
@Environment(\.modelContext)
19+
private var modelContext
20+
1821
init() {
1922
let predicate = #Predicate<SavedItem> { $0.isArchived == false && $0.deletedAt == nil }
2023
let sortDescriptor = SortDescriptor<SavedItem>(\.createdAt, order: .reverse)
@@ -67,7 +70,7 @@ private extension RecentSavesView {
6770

6871
var proposedCards: [HomeCardConfiguration] {
6972
savedItems.enumerated().compactMap {
70-
guard let item = $0.element.item else {
73+
guard let remoteID = $0.element.remoteID, let item = fetchItem(remoteID) else {
7174
return nil
7275
}
7376
return HomeCardConfiguration(
@@ -91,4 +94,16 @@ private extension RecentSavesView {
9194
)
9295
}
9396
}
97+
98+
/// Fetch an `Item` from the underlying `SavedItem`
99+
/// - Parameter recommendationID: `SavedItem` ID
100+
/// - Returns: the item, if it was found
101+
func fetchItem(_ savedItemID: String) -> Item? {
102+
let predicate = #Predicate<Item> { $0.savedItem?.remoteID == savedItemID }
103+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
104+
fetchDescriptor.fetchLimit = 1
105+
106+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
107+
return result.first
108+
}
94109
}

PocketKit/Sources/PocketKit/Home/Views/Top level views/RecommendationsView.swift

+18-13
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,15 @@ private extension RecommendationsView {
8383
makeOfflineView()
8484
}
8585
}
86-
private func slateInfo(_ slate: Slate) -> SlateInfo? {
87-
guard let lineup = slate.slateLineup else { return nil }
88-
return SlateInfo(
89-
slateId: slate.remoteID,
90-
slateRequestId: slate.requestID,
91-
slateExperimentId: slate.experimentID,
92-
slateIndex: Int(slate.sortIndex ?? 0),
93-
slateLineupId: lineup.remoteID
94-
)
95-
}
86+
9687
@ViewBuilder
9788
func makeSlatesView() -> some View {
9889
ForEach(slates) {
9990
if let recommendations = $0.recommendations, !recommendations.isEmpty {
10091
SlateView(
10192
remoteID: $0.remoteID,
10293
slateTitle: $0.name,
103-
cards: cards(for: $0.remoteID),
104-
slateInfo: slateInfo($0)
94+
cards: cards(for: $0.remoteID)
10595
)
10696
}
10797
}
@@ -115,6 +105,9 @@ private extension RecommendationsView {
115105
OfflineView()
116106
}
117107

108+
/// Fetch `Recommendation`s of the current `Slate`
109+
/// - Parameter slateID: `Slate` ID
110+
/// - Returns: the collection of `Recommendation`s, limited to 6 elements.
118111
func fetchRecommendations(_ slateID: String) -> [Recommendation] {
119112
let predicate = #Predicate<Recommendation> { $0.slate?.remoteID == slateID }
120113
let sortDescriptor = SortDescriptor<Recommendation>(\.sortIndex, order: .forward)
@@ -124,10 +117,22 @@ private extension RecommendationsView {
124117
return (try? modelContext.fetch(fetchDescriptor)) ?? []
125118
}
126119

120+
/// Fetch an `Item` from the underlying `Recommendation`
121+
/// - Parameter recommendationID: `Recommendation` ID
122+
/// - Returns: the item, if it was found
123+
func fetchItem(_ recommendationID: String) -> Item? {
124+
let predicate = #Predicate<Item> { $0.recommendation?.remoteID == recommendationID }
125+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
126+
fetchDescriptor.fetchLimit = 1
127+
128+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
129+
return result.first
130+
}
131+
127132
func cards(for slateID: String) -> [HomeCardConfiguration] {
128133
fetchRecommendations(slateID)
129134
.compactMap {
130-
if let item = $0.item {
135+
if let item = fetchItem($0.remoteID) {
131136
return HomeCardConfiguration(
132137
givenURL: item.givenURL,
133138
sharedWithYouUrlString: nil,

PocketKit/Sources/PocketKit/Home/Views/Top level views/SharedWithYouView.swift

+27-11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ struct SharedWithYouView: View {
1616

1717
@State private var cards: [HomeCardConfiguration] = []
1818

19+
@Environment(\.modelContext)
20+
private var modelContext
21+
1922
init() {
2023
let sortDescriptor = SortDescriptor<SharedWithYouItem>(\.sortOrder, order: .forward)
2124
var fetchDescriptor = FetchDescriptor<SharedWithYouItem>(sortBy: [sortDescriptor])
@@ -63,22 +66,35 @@ private extension SharedWithYouView {
6366
.padding(.trailing, 16)
6467
}
6568

69+
/// Fetch an `Item` from the underlying `SharedWithYouItem`
70+
/// - Parameter sharedWithYouUrl: `SharedWithYouItem` url
71+
/// - Returns: the item, if it was found
72+
func fetchItem(_ sharedWithYouUrl: String) -> Item? {
73+
let predicate = #Predicate<Item> { $0.sharedWithYouItem?.url == sharedWithYouUrl }
74+
var fetchDescriptor = FetchDescriptor(predicate: predicate)
75+
fetchDescriptor.fetchLimit = 1
76+
77+
let result = (try? modelContext.fetch(fetchDescriptor)) ?? []
78+
return result.first
79+
}
80+
6681
var proposedCards: [HomeCardConfiguration] {
6782
sharedWithYouItems.enumerated().compactMap {
68-
HomeCardConfiguration(
69-
givenURL: $0.element.item?.givenURL ?? $0.element.url,
83+
guard let item = fetchItem($0.element.url) else { return nil }
84+
return HomeCardConfiguration(
85+
givenURL: item.givenURL,
7086
sharedWithYouUrlString: $0.element.url,
7187
type: .sharedWithYou,
7288
index: $0.offset,
73-
shareURL: $0.element.item?.shareURL,
74-
domain: $0.element.item?.bestDomain,
75-
timeToRead: $0.element.item?.timeToRead,
76-
isSyndicated: $0.element.item?.isSyndicated == true,
77-
recommendationID: $0.element.item?.recommendation?.analyticsID,
78-
bestTitle: $0.element.item?.bestTitle,
79-
slug: $0.element.item?.collectionSlug,
80-
excerpt: $0.element.item?.excerpt,
81-
topImageURL: $0.element.item?.topImageURL,
89+
shareURL: item.shareURL,
90+
domain: item.bestDomain,
91+
timeToRead: item.timeToRead,
92+
isSyndicated: item.isSyndicated == true,
93+
recommendationID: item.recommendation?.analyticsID,
94+
bestTitle: item.bestTitle,
95+
slug: item.collectionSlug,
96+
excerpt: item.excerpt,
97+
topImageURL: item.topImageURL,
8298
enableSaveAction: true,
8399
enableShareMenuAction: true,
84100
enableReportMenuAction: true

0 commit comments

Comments
 (0)