From 1d4a9a01c43c1fc5e1062bbff70fa5fc28548b66 Mon Sep 17 00:00:00 2001 From: Gio <34376330+Gio2018@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:30:45 -0600 Subject: [PATCH] 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 --- .../Home/Views/Card groups/SlateView.swift | 3 +- .../SharedWithYouDetailView.swift | 18 +++++- .../Views/Detail views/SlateDetailView.swift | 63 ++++++++++++++++++- .../Navigation/NavigationDestination.swift | 1 - .../Top level views/RecentSavesView.swift | 17 ++++- .../Top level views/RecommendationsView.swift | 31 +++++---- .../Top level views/SharedWithYouView.swift | 38 +++++++---- 7 files changed, 140 insertions(+), 31 deletions(-) diff --git a/PocketKit/Sources/PocketKit/Home/Views/Card groups/SlateView.swift b/PocketKit/Sources/PocketKit/Home/Views/Card groups/SlateView.swift index 7297c06bd..2c9b79a1e 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Card groups/SlateView.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Card groups/SlateView.swift @@ -11,7 +11,6 @@ struct SlateView: View { let remoteID: String let slateTitle: String? let cards: [HomeCardConfiguration] - let slateInfo: SlateInfo? @Environment(\.layoutWidth) private var layoutWidth @@ -38,7 +37,7 @@ struct SlateView: View { private extension SlateView { func makeHeader(_ title: String) -> some View { SectionHeader(title: title) { - navigation.navigateTo(SlateDestination(slateID: remoteID, slateTitle: slateTitle, slateInfo: slateInfo)) + navigation.navigateTo(SlateDestination(slateID: remoteID, slateTitle: slateTitle)) } } diff --git a/PocketKit/Sources/PocketKit/Home/Views/Detail views/SharedWithYouDetailView.swift b/PocketKit/Sources/PocketKit/Home/Views/Detail views/SharedWithYouDetailView.swift index 10a6372c2..a216d4b41 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Detail views/SharedWithYouDetailView.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Detail views/SharedWithYouDetailView.swift @@ -20,6 +20,9 @@ struct SharedWithYouDetailView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.modelContext) + private var modelContext + var body: some View { GeometryReader { proxy in CardCollection(cards: cards, size: .large, layoutWidth: layoutWidth(proxy.size)) @@ -39,7 +42,7 @@ struct SharedWithYouDetailView: View { private extension SharedWithYouDetailView { var proposedCards: [HomeCardConfiguration] { sharedWithYouItems.enumerated().compactMap { - if let item = $0.element.item { + if let item = fetchItem($0.element.url) { return HomeCardConfiguration( givenURL: item.givenURL, sharedWithYouUrlString: $0.element.url, @@ -62,6 +65,19 @@ private extension SharedWithYouDetailView { return nil } } + + /// Fetch an `Item` from the underlying `SharedWithYouItem` + /// - Parameter sharedWithYouUrl: `SharedWithYouItem` url + /// - Returns: the item, if it was found + func fetchItem(_ sharedWithYouUrl: String) -> Item? { + let predicate = #Predicate { $0.sharedWithYouItem?.url == sharedWithYouUrl } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } + /// Determine the size of the current layout /// **NOTE: turns out that, since this is a detail view, the environment value `layoutWidth` /// cannot be used here since the GeometryReader of HomeView is not active diff --git a/PocketKit/Sources/PocketKit/Home/Views/Detail views/SlateDetailView.swift b/PocketKit/Sources/PocketKit/Home/Views/Detail views/SlateDetailView.swift index 59109f34b..af767043c 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Detail views/SlateDetailView.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Detail views/SlateDetailView.swift @@ -19,6 +19,9 @@ struct SlateDetailView: View { @Environment(\.homeActions) private var homeActions + @Environment(\.modelContext) + private var modelContext + init(destination: SlateDestination) { self.destination = destination let slateID = destination.slateID @@ -41,7 +44,7 @@ struct SlateDetailView: View { } } .onAppear { - guard let slateInfo = destination.slateInfo else { + guard let slateInfo = slateInfo(destination) else { return } homeActions.trackSlateDetailImpression(info: slateInfo) @@ -56,7 +59,7 @@ struct SlateDetailView: View { private extension SlateDetailView { var proposedCards: [HomeCardConfiguration] { recommendations.enumerated().compactMap { - if let item = $0.element.item { + if let item = fetchItem($0.element.remoteID) { return HomeCardConfiguration( givenURL: item.givenURL, sharedWithYouUrlString: nil, @@ -79,6 +82,62 @@ private extension SlateDetailView { return nil } } + + /// Fetch analytics info for this slate + /// - Parameter destination: slate destination of this slate + /// - Returns: analytics info + func slateInfo(_ destination: SlateDestination) -> SlateInfo? { + guard let slate = fetchSlate(destination.slateID), + let lineup = fetchSlateLineup() else { + return nil + } + return SlateInfo( + slateId: slate.remoteID, + slateRequestId: slate.requestID, + slateExperimentId: slate.experimentID, + slateIndex: Int(slate.sortIndex ?? 0), + slateLineupId: lineup.remoteID + ) + } + + /// Fetch an `Item` from the underlying `Recommendation` + /// - Parameter recommendationID: `Recommendation` ID + /// - Returns: the item, if it was found + func fetchItem(_ recommendationID: String) -> Item? { + let predicate = #Predicate { $0.recommendation?.remoteID == recommendationID } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } + + /// Fetch the current slate from SwiftData + /// - Parameter remoteID: the remote id of this slate + /// - Returns: the slate, if it was found + func fetchSlate(_ remoteID: String) -> Slate? { + let predicate = #Predicate { $0.remoteID == remoteID } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } + + /// Fettch the current slate lineup + /// - Returns: the slate lineup, if it was found + func fetchSlateLineup() -> SlateLineup? { + // there is only one lineup, so we don't need to filter this query + let predicate = #Predicate { _ in + return true + } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } + /// Determine the size of the current layout /// **NOTE: turns out that, since this is a detail view, the environment value `layoutWidth` /// cannot be used here since the GeometryReader of HomeView is not active diff --git a/PocketKit/Sources/PocketKit/Home/Views/Navigation/NavigationDestination.swift b/PocketKit/Sources/PocketKit/Home/Views/Navigation/NavigationDestination.swift index e074fa255..288d94eda 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Navigation/NavigationDestination.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Navigation/NavigationDestination.swift @@ -10,7 +10,6 @@ protocol NavigationDestination: Codable, Hashable {} struct SlateDestination: NavigationDestination { let slateID: String let slateTitle: String? - let slateInfo: SlateInfo? } struct NativeCollectionDestination: NavigationDestination { diff --git a/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecentSavesView.swift b/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecentSavesView.swift index 8c3f5f54c..fbcac39e2 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecentSavesView.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecentSavesView.swift @@ -15,6 +15,9 @@ struct RecentSavesView: View { @State private var cards: [HomeCardConfiguration] = [] + @Environment(\.modelContext) + private var modelContext + init() { let predicate = #Predicate { $0.isArchived == false && $0.deletedAt == nil } let sortDescriptor = SortDescriptor(\.createdAt, order: .reverse) @@ -67,7 +70,7 @@ private extension RecentSavesView { var proposedCards: [HomeCardConfiguration] { savedItems.enumerated().compactMap { - guard let item = $0.element.item else { + guard let remoteID = $0.element.remoteID, let item = fetchItem(remoteID) else { return nil } return HomeCardConfiguration( @@ -91,4 +94,16 @@ private extension RecentSavesView { ) } } + + /// Fetch an `Item` from the underlying `SavedItem` + /// - Parameter recommendationID: `SavedItem` ID + /// - Returns: the item, if it was found + func fetchItem(_ savedItemID: String) -> Item? { + let predicate = #Predicate { $0.savedItem?.remoteID == savedItemID } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } } diff --git a/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecommendationsView.swift b/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecommendationsView.swift index aa3ce21aa..1ab3ef63b 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecommendationsView.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Top level views/RecommendationsView.swift @@ -83,16 +83,7 @@ private extension RecommendationsView { makeOfflineView() } } - private func slateInfo(_ slate: Slate) -> SlateInfo? { - guard let lineup = slate.slateLineup else { return nil } - return SlateInfo( - slateId: slate.remoteID, - slateRequestId: slate.requestID, - slateExperimentId: slate.experimentID, - slateIndex: Int(slate.sortIndex ?? 0), - slateLineupId: lineup.remoteID - ) - } + @ViewBuilder func makeSlatesView() -> some View { ForEach(slates) { @@ -100,8 +91,7 @@ private extension RecommendationsView { SlateView( remoteID: $0.remoteID, slateTitle: $0.name, - cards: cards(for: $0.remoteID), - slateInfo: slateInfo($0) + cards: cards(for: $0.remoteID) ) } } @@ -115,6 +105,9 @@ private extension RecommendationsView { OfflineView() } + /// Fetch `Recommendation`s of the current `Slate` + /// - Parameter slateID: `Slate` ID + /// - Returns: the collection of `Recommendation`s, limited to 6 elements. func fetchRecommendations(_ slateID: String) -> [Recommendation] { let predicate = #Predicate { $0.slate?.remoteID == slateID } let sortDescriptor = SortDescriptor(\.sortIndex, order: .forward) @@ -124,10 +117,22 @@ private extension RecommendationsView { return (try? modelContext.fetch(fetchDescriptor)) ?? [] } + /// Fetch an `Item` from the underlying `Recommendation` + /// - Parameter recommendationID: `Recommendation` ID + /// - Returns: the item, if it was found + func fetchItem(_ recommendationID: String) -> Item? { + let predicate = #Predicate { $0.recommendation?.remoteID == recommendationID } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } + func cards(for slateID: String) -> [HomeCardConfiguration] { fetchRecommendations(slateID) .compactMap { - if let item = $0.item { + if let item = fetchItem($0.remoteID) { return HomeCardConfiguration( givenURL: item.givenURL, sharedWithYouUrlString: nil, diff --git a/PocketKit/Sources/PocketKit/Home/Views/Top level views/SharedWithYouView.swift b/PocketKit/Sources/PocketKit/Home/Views/Top level views/SharedWithYouView.swift index e2d0b8a97..bbd87d2b5 100644 --- a/PocketKit/Sources/PocketKit/Home/Views/Top level views/SharedWithYouView.swift +++ b/PocketKit/Sources/PocketKit/Home/Views/Top level views/SharedWithYouView.swift @@ -16,6 +16,9 @@ struct SharedWithYouView: View { @State private var cards: [HomeCardConfiguration] = [] + @Environment(\.modelContext) + private var modelContext + init() { let sortDescriptor = SortDescriptor(\.sortOrder, order: .forward) var fetchDescriptor = FetchDescriptor(sortBy: [sortDescriptor]) @@ -63,22 +66,35 @@ private extension SharedWithYouView { .padding(.trailing, 16) } + /// Fetch an `Item` from the underlying `SharedWithYouItem` + /// - Parameter sharedWithYouUrl: `SharedWithYouItem` url + /// - Returns: the item, if it was found + func fetchItem(_ sharedWithYouUrl: String) -> Item? { + let predicate = #Predicate { $0.sharedWithYouItem?.url == sharedWithYouUrl } + var fetchDescriptor = FetchDescriptor(predicate: predicate) + fetchDescriptor.fetchLimit = 1 + + let result = (try? modelContext.fetch(fetchDescriptor)) ?? [] + return result.first + } + var proposedCards: [HomeCardConfiguration] { sharedWithYouItems.enumerated().compactMap { - HomeCardConfiguration( - givenURL: $0.element.item?.givenURL ?? $0.element.url, + guard let item = fetchItem($0.element.url) else { return nil } + return HomeCardConfiguration( + givenURL: item.givenURL, sharedWithYouUrlString: $0.element.url, type: .sharedWithYou, index: $0.offset, - shareURL: $0.element.item?.shareURL, - domain: $0.element.item?.bestDomain, - timeToRead: $0.element.item?.timeToRead, - isSyndicated: $0.element.item?.isSyndicated == true, - recommendationID: $0.element.item?.recommendation?.analyticsID, - bestTitle: $0.element.item?.bestTitle, - slug: $0.element.item?.collectionSlug, - excerpt: $0.element.item?.excerpt, - topImageURL: $0.element.item?.topImageURL, + shareURL: item.shareURL, + domain: item.bestDomain, + timeToRead: item.timeToRead, + isSyndicated: item.isSyndicated == true, + recommendationID: item.recommendation?.analyticsID, + bestTitle: item.bestTitle, + slug: item.collectionSlug, + excerpt: item.excerpt, + topImageURL: item.topImageURL, enableSaveAction: true, enableShareMenuAction: true, enableReportMenuAction: true