From ea2ba678c09b0c96d204eb206d1e2e6059a911c6 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 14 Apr 2022 13:35:28 -0700 Subject: [PATCH 1/4] add fetchViewer async func --- .../App/Views/Profile/ProfileView.swift | 26 +++++------- .../DataService/Queries/ViewerPublisher.swift | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 80d6eef94..aab2f65ab 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -5,12 +5,10 @@ import SwiftUI import Utils import Views -final class ProfileContainerViewModel: ObservableObject { +@MainActor final class ProfileContainerViewModel: ObservableObject { @Published var isLoading = false @Published var profileCardData = ProfileCardData() - var subscriptions = Set() - var appVersionString: String { if let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { return "Omnivore Version \(appVersion)" @@ -19,18 +17,14 @@ final class ProfileContainerViewModel: ObservableObject { } } - func loadProfileData(dataService: DataService) { - dataService.viewerPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] viewer in - self?.profileCardData = ProfileCardData( - name: viewer.name, - username: viewer.username, - imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } - ) - } + func loadProfileData(dataService: DataService) async { + guard let viewer = try? await dataService.fetchViewer() else { return } + + profileCardData = ProfileCardData( + name: viewer.name, + username: viewer.username, + imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } ) - .store(in: &subscriptions) } } @@ -59,7 +53,9 @@ struct ProfileView: View { Group { Section { ProfileCard(data: viewModel.profileCardData) - .onAppear { viewModel.loadProfileData(dataService: dataService) } + .task { + await viewModel.loadProfileData(dataService: dataService) + } } Section { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift index 7b91e2c50..39961b7cb 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift @@ -5,6 +5,47 @@ import SwiftGraphQL import Utils public extension DataService { + func fetchViewer() async throws -> Viewer { + let selection = Selection { + Viewer( + username: try $0.profile( + selection: .init { try $0.username() } + ), + name: try $0.name(), + profileImageURL: try $0.profile( + selection: .init { try $0.pictureUrl() } + ), + userID: try $0.id() + ) + } + + let query = Selection.Query { + try $0.me(selection: selection.nonNullOrFail) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { [weak self] result in + switch result { + case let .success(payload): + self?.currentViewer = payload.data + if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil { + UserDefaults.standard.setValue(payload.data.userID, forKey: Keys.userIdKey) + DataService.registerIntercomUser?(payload.data.userID) + } + continuation.resume(returning: payload.data) + case .failure: + continuation.resume(throwing: BasicError.message(messageText: "http error")) + } + } + } + } +} + +public extension DataService { + @available(*, deprecated, message: "use async version instead") func viewerPublisher() -> AnyPublisher { internalViewerPublisher() .handleEvents(receiveOutput: { From 5dff87a8e9e1732f42ee5a10eed9acc74911144a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 14 Apr 2022 13:55:46 -0700 Subject: [PATCH 2/4] replace uses of viewerPublisher with fetchViewer --- .../Components/FeedCardNavigationLink.swift | 4 +- .../App/Views/Home/HomeFeedViewIOS.swift | 14 +++---- .../App/Views/Home/HomeFeedViewModel.swift | 14 +++---- .../App/Views/LinkItemDetailView.swift | 41 ++++++++----------- 4 files changed, 30 insertions(+), 43 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index dbf0084ca..93fd2670b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -22,7 +22,7 @@ struct FeedCardNavigationLink: View { .opacity(0) .buttonStyle(PlainButtonStyle()) .onAppear { - viewModel.itemAppeared(item: item, dataService: dataService) + Task { await viewModel.itemAppeared(item: item, dataService: dataService) } } FeedCard(item: item) } @@ -60,7 +60,7 @@ struct GridCardNavigationLink: View { } }) .onAppear { - viewModel.itemAppeared(item: item, dataService: dataService) + Task { await viewModel.itemAppeared(item: item, dataService: dataService) } } } .aspectRatio(1.8, contentMode: .fill) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 46720c206..f5462772b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -19,7 +19,7 @@ import Views viewModel: viewModel ) .refreshable { - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } .searchable( text: $viewModel.searchTerm, @@ -35,13 +35,13 @@ import Views .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onChange(of: viewModel.selectedLabels) { _ in - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onSubmit(of: .search) { - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(mode: .item(item)) { labels in @@ -54,7 +54,7 @@ import Views .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in // Don't refresh the list if the user is currently reading an article if viewModel.selectedLinkItem == nil { - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } } .onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in @@ -75,7 +75,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } } .onChange(of: viewModel.selectedLinkItem) { _ in @@ -322,7 +322,7 @@ import Views .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in DispatchQueue.main.async { if !viewModel.isLoading, offset > 240 { - viewModel.loadItems(dataService: dataService, isRefresh: true) + Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 4ee67a777..e3bfffbc8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -5,7 +5,7 @@ import SwiftUI import Utils import Views -final class HomeFeedViewModel: ObservableObject { +@MainActor final class HomeFeedViewModel: ObservableObject { var currentDetailViewModel: LinkItemDetailViewModel? /// Track progress updates to be committed when user navigates back to grid view @@ -36,14 +36,14 @@ final class HomeFeedViewModel: ObservableObject { init() {} - func itemAppeared(item: FeedItem, dataService: DataService) { + func itemAppeared(item: FeedItem, dataService: DataService) async { if isLoading { return } let itemIndex = items.firstIndex(where: { $0.id == item.id }) let thresholdIndex = items.index(items.endIndex, offsetBy: -5) // Check if user has scrolled to the last five items in the list if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { - loadItems(dataService: dataService, isRefresh: false) + await loadItems(dataService: dataService, isRefresh: false) } } @@ -51,7 +51,7 @@ final class HomeFeedViewModel: ObservableObject { items.insert(item, at: 0) } - func loadItems(dataService: DataService, isRefresh: Bool) { + func loadItems(dataService: DataService, isRefresh: Bool) async { // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set dataService.clearHighlights() @@ -62,11 +62,7 @@ final class HomeFeedViewModel: ObservableObject { // Cache the viewer if dataService.currentViewer == nil { - dataService.viewerPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { _ in } - ) - .store(in: &subscriptions) + _ = try? await dataService.fetchViewer() } dataService.libraryItemsPublisher( diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index d267389e3..6ae0f7ad9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -9,7 +9,7 @@ enum PDFProvider { static var pdfViewerProvider: ((URL, FeedItem) -> AnyView)? } -final class LinkItemDetailViewModel: ObservableObject { +@MainActor final class LinkItemDetailViewModel: ObservableObject { let homeFeedViewModel: HomeFeedViewModel @Published var item: FeedItem @Published var webAppWrapperViewModel: WebAppWrapperViewModel? @@ -45,31 +45,22 @@ final class LinkItemDetailViewModel: ObservableObject { .store(in: &subscriptions) } - func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) { - // Attempt to get `Viewer` from DataService - if let currentViewer = dataService.currentViewer { + func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) async { + let viewer: Viewer? = await { + if let currentViewer = dataService.currentViewer { + return currentViewer + } + + return try? await dataService.fetchViewer() + }() + + if let viewer = viewer { createWebAppWrapperViewModel( - username: currentViewer.username, + username: viewer.username, dataService: dataService, rawAuthCookie: rawAuthCookie ) - return } - - dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - self?.createWebAppWrapperViewModel( - username: viewer.username, - dataService: dataService, - rawAuthCookie: rawAuthCookie - ) - } - ) - .store(in: &subscriptions) } private func createWebAppWrapperViewModel(username: String, dataService: DataService, rawAuthCookie: String?) { @@ -265,8 +256,8 @@ struct LinkItemDetailView: View { navBar Spacer() } - .onAppear { - viewModel.loadWebAppWrapper( + .task { + await viewModel.loadWebAppWrapper( dataService: dataService, rawAuthCookie: authenticator.omnivoreAuthCookieString ) @@ -311,8 +302,8 @@ struct LinkItemDetailView: View { Text("Loading...") Spacer() } - .onAppear { - viewModel.loadWebAppWrapper( + .task { + await viewModel.loadWebAppWrapper( dataService: dataService, rawAuthCookie: authenticator.omnivoreAuthCookieString ) From 70c4b1c690a5ca46f57e5091c98b3f471d1ca6c5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 14 Apr 2022 14:57:48 -0700 Subject: [PATCH 3/4] convery additional uses of viewerPublisher to fetchViewer --- .../Share/ShareExtensionScene.swift | 15 ++++++++------- .../Sources/App/Views/RootView/RootView.swift | 4 ++-- .../App/Views/RootView/RootViewModel.swift | 19 +++++-------------- ...werPublisher.swift => ViewerFetcher.swift} | 17 +---------------- 4 files changed, 16 insertions(+), 39 deletions(-) rename apple/OmnivoreKit/Sources/Services/DataService/Queries/{ViewerPublisher.swift => ViewerFetcher.swift} (81%) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 42f2f3225..acc01f73a 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -80,13 +80,14 @@ final class ShareExtensionViewModel: ObservableObject { .store(in: &subscriptions) // Using viewerPublisher to get fast feedback for auth/network errors - services.dataService.viewerPublisher() - .sink { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.debugText = "saveArticleError: \(error)" - self?.status = .failed(error: .unknown(description: "")) - } receiveValue: { _ in } - .store(in: &subscriptions) + Task { + do { + _ = try await services.dataService.fetchViewer() + } catch { + debugText = "saveArticleError: \(error)" + status = .failed(error: .unknown(description: "")) + } + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 0d4af5f7c..135be02c1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -91,10 +91,10 @@ struct InnerRootView: View { if viewModel.webLinkPath != nil { viewModel.webLinkPath = nil DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - viewModel.onOpenURL(url: url) + Task { await viewModel.onOpenURL(url: url) } } } else { - viewModel.onOpenURL(url: url) + Task { await viewModel.onOpenURL(url: url) } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index fa8762ec0..a8afdf799 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -20,8 +20,6 @@ public final class RootViewModel: ObservableObject { @Published var snackbarMessage: String? @Published var showSnackbar = false - public var subscriptions = Set() - public init() { registerFonts() @@ -57,7 +55,7 @@ public final class RootViewModel: ObservableObject { ) } - func onOpenURL(url: URL) { + @MainActor func onOpenURL(url: URL) async { guard let linkRequestID = DeepLink.make(from: url)?.linkRequestID else { return } if let username = services.dataService.currentViewer?.username { @@ -66,17 +64,10 @@ public final class RootViewModel: ObservableObject { return } - services.dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - let path = self?.linkRequestPath(username: viewer.username, requestID: linkRequestID) ?? "" - self?.webLinkPath = SafariWebLinkPath(id: UUID(), path: path) - } - ) - .store(in: &subscriptions) + if let viewer = try? await services.dataService.fetchViewer() { + let path = linkRequestPath(username: viewer.username, requestID: linkRequestID) + webLinkPath = SafariWebLinkPath(id: UUID(), path: path) + } } func triggerPushNotificationRequestIfNeeded() { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift similarity index 81% rename from apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift rename to apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift index 39961b7cb..509ff01a7 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift @@ -44,23 +44,8 @@ public extension DataService { } } -public extension DataService { - @available(*, deprecated, message: "use async version instead") - func viewerPublisher() -> AnyPublisher { - internalViewerPublisher() - .handleEvents(receiveOutput: { - // Persist ID so AppDelegate can use it to register Intercom users at launch time - if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil { - UserDefaults.standard.setValue($0.userID, forKey: Keys.userIdKey) - DataService.registerIntercomUser?($0.userID) - } - }) - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } -} - extension DataService { + @available(*, deprecated, message: "use async version instead") func internalViewerPublisher() -> AnyPublisher { let selection = Selection { Viewer( From 98080719f37e58f2e746f5ceefa5fbc65a2a2a80 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 14 Apr 2022 15:06:50 -0700 Subject: [PATCH 4/4] wrap viewModel async call in Task --- .../Home/Components/FeedCardNavigationLink.swift | 4 ++-- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 14 +++++++------- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 9 +++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 93fd2670b..dbf0084ca 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -22,7 +22,7 @@ struct FeedCardNavigationLink: View { .opacity(0) .buttonStyle(PlainButtonStyle()) .onAppear { - Task { await viewModel.itemAppeared(item: item, dataService: dataService) } + viewModel.itemAppeared(item: item, dataService: dataService) } FeedCard(item: item) } @@ -60,7 +60,7 @@ struct GridCardNavigationLink: View { } }) .onAppear { - Task { await viewModel.itemAppeared(item: item, dataService: dataService) } + viewModel.itemAppeared(item: item, dataService: dataService) } } .aspectRatio(1.8, contentMode: .fill) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index f5462772b..46720c206 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -19,7 +19,7 @@ import Views viewModel: viewModel ) .refreshable { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } .searchable( text: $viewModel.searchTerm, @@ -35,13 +35,13 @@ import Views .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } .onChange(of: viewModel.selectedLabels) { _ in - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } .onSubmit(of: .search) { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(mode: .item(item)) { labels in @@ -54,7 +54,7 @@ import Views .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in // Don't refresh the list if the user is currently reading an article if viewModel.selectedLinkItem == nil { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in @@ -75,7 +75,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onChange(of: viewModel.selectedLinkItem) { _ in @@ -322,7 +322,7 @@ import Views .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in DispatchQueue.main.async { if !viewModel.isLoading, offset > 240 { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: true) } + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index e3bfffbc8..ed61a4341 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -36,14 +36,14 @@ import Views init() {} - func itemAppeared(item: FeedItem, dataService: DataService) async { + func itemAppeared(item: FeedItem, dataService: DataService) { if isLoading { return } let itemIndex = items.firstIndex(where: { $0.id == item.id }) let thresholdIndex = items.index(items.endIndex, offsetBy: -5) // Check if user has scrolled to the last five items in the list if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { - await loadItems(dataService: dataService, isRefresh: false) + Task { await loadItems(dataService: dataService, isRefresh: false) } } } @@ -51,7 +51,7 @@ import Views items.insert(item, at: 0) } - func loadItems(dataService: DataService, isRefresh: Bool) async { + func loadItems(dataService: DataService, isRefresh: Bool) { // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set dataService.clearHighlights() @@ -61,8 +61,9 @@ import Views isLoading = true // Cache the viewer + if dataService.currentViewer == nil { - _ = try? await dataService.fetchViewer() + Task { _ = try? await dataService.fetchViewer() } } dataService.libraryItemsPublisher(