convery items publisher to async func
This commit is contained in:
@ -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)
|
||||
|
||||
@ -12,6 +12,10 @@ import Views
|
||||
@AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = UIDevice.isIPhone
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
func loadItems(isRefresh: Bool) {
|
||||
Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
HomeFeedView(
|
||||
@ -19,7 +23,7 @@ import Views
|
||||
viewModel: viewModel
|
||||
)
|
||||
.refreshable {
|
||||
viewModel.loadItems(dataService: dataService, isRefresh: true)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.searchable(
|
||||
text: $viewModel.searchTerm,
|
||||
@ -35,13 +39,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)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.selectedLabels) { _ in
|
||||
viewModel.loadItems(dataService: dataService, isRefresh: true)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onSubmit(of: .search) {
|
||||
viewModel.loadItems(dataService: dataService, isRefresh: true)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
|
||||
ApplyLabelsView(mode: .item(item)) { _ in }
|
||||
@ -52,7 +56,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)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushJSONArticle"))) { notification in
|
||||
@ -72,9 +76,9 @@ import Views
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.onAppear { // TODO: use task instead
|
||||
if viewModel.items.isEmpty {
|
||||
viewModel.loadItems(dataService: dataService, isRefresh: true)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -284,6 +288,10 @@ import Views
|
||||
}
|
||||
}
|
||||
|
||||
func loadItems(isRefresh: Bool) {
|
||||
Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 325), spacing: 24)], spacing: 24) {
|
||||
@ -319,7 +327,7 @@ import Views
|
||||
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
|
||||
DispatchQueue.main.async {
|
||||
if !viewModel.isLoading, offset > 240 {
|
||||
viewModel.loadItems(dataService: dataService, isRefresh: true)
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,14 +30,14 @@ import Views
|
||||
|
||||
init() {}
|
||||
|
||||
func itemAppeared(item: LinkedItem, dataService: DataService) {
|
||||
func itemAppeared(item: LinkedItem, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,66 +45,59 @@ import Views
|
||||
items.insert(item, at: 0)
|
||||
}
|
||||
|
||||
func loadItems(dataService: DataService, isRefresh: Bool) {
|
||||
func loadItems(dataService: DataService, isRefresh: Bool) async {
|
||||
let thisSearchIdx = searchIdx
|
||||
searchIdx += 1
|
||||
|
||||
isLoading = true
|
||||
|
||||
// Cache the viewer
|
||||
|
||||
if dataService.currentViewer == nil {
|
||||
Task { _ = try? await dataService.fetchViewer() }
|
||||
}
|
||||
|
||||
// TODO: fix issues with this list
|
||||
dataService.libraryItemsPublisher(
|
||||
let queryResult = try? await dataService.fetchLinkedItems(
|
||||
limit: 10,
|
||||
sortDescending: true,
|
||||
searchQuery: searchQuery,
|
||||
cursor: isRefresh ? nil : cursor
|
||||
)
|
||||
.sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case .failure = completion else { return }
|
||||
dataService.viewContext.perform {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)]
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "serverSyncStatus != %i", Int64(ServerSyncStatus.needsDeletion.rawValue)
|
||||
)
|
||||
if let fetchedItems = try? dataService.viewContext.fetch(fetchRequest) {
|
||||
self?.items = fetchedItems
|
||||
self?.cursor = nil
|
||||
self?.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveValue: { [weak self] result in
|
||||
// Search results aren't guaranteed to return in order so this
|
||||
// will discard old results that are returned while a user is typing.
|
||||
// For example if a user types 'Canucks', often the search results
|
||||
// for 'C' are returned after 'Canucks' because it takes the backend
|
||||
// much longer to compute.
|
||||
if thisSearchIdx > 0, thisSearchIdx <= self?.receivedIdx ?? 0 {
|
||||
return
|
||||
}
|
||||
|
||||
self?.items = {
|
||||
let itemIDs = isRefresh ? result.items : (self?.items ?? []).map(\.objectID) + result.items
|
||||
var itemObjects = [LinkedItem]()
|
||||
dataService.viewContext.performAndWait {
|
||||
itemObjects = itemIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem }
|
||||
}
|
||||
return itemObjects
|
||||
}()
|
||||
dataService.prefetchPages(itemSlugs: (self?.items ?? []).map(\.unwrappedSlug))
|
||||
self?.isLoading = false
|
||||
self?.receivedIdx = thisSearchIdx
|
||||
self?.cursor = result.cursor
|
||||
// Search results aren't guaranteed to return in order so this
|
||||
// will discard old results that are returned while a user is typing.
|
||||
// For example if a user types 'Canucks', often the search results
|
||||
// for 'C' are returned after 'Canucks' because it takes the backend
|
||||
// much longer to compute.
|
||||
if thisSearchIdx > 0, thisSearchIdx <= receivedIdx {
|
||||
return
|
||||
}
|
||||
|
||||
if let queryResult = queryResult {
|
||||
items = {
|
||||
let itemIDs = isRefresh ? queryResult.items : items.map(\.objectID) + queryResult.items
|
||||
var itemObjects = [LinkedItem]()
|
||||
dataService.viewContext.performAndWait {
|
||||
itemObjects = itemIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem }
|
||||
}
|
||||
return itemObjects
|
||||
}()
|
||||
dataService.prefetchPages(itemSlugs: items.map(\.unwrappedSlug))
|
||||
isLoading = false
|
||||
receivedIdx = thisSearchIdx
|
||||
cursor = queryResult.cursor
|
||||
} else {
|
||||
await dataService.viewContext.perform {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)]
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "serverSyncStatus != %i", Int64(ServerSyncStatus.needsDeletion.rawValue)
|
||||
)
|
||||
if let fetchedItems = try? dataService.viewContext.fetch(fetchRequest) {
|
||||
self.items = fetchedItems
|
||||
self.cursor = nil
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public struct HomeFeedData {
|
||||
public struct HomeFeedData { // TODO: rename this
|
||||
public let items: [NSManagedObjectID]
|
||||
public let cursor: String?
|
||||
|
||||
|
||||
@ -36,7 +36,6 @@ extension DataService {
|
||||
}
|
||||
|
||||
private func syncLinkedItems(unsyncedLinkedItems: [LinkedItem]) {
|
||||
logger.debug("SYNCHINGGG")
|
||||
for item in unsyncedLinkedItems {
|
||||
guard let syncStatus = ServerSyncStatus(rawValue: Int(item.serverSyncStatus)) else { continue }
|
||||
|
||||
|
||||
@ -5,17 +5,13 @@ import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
// swiftlint:disable:next function_body_length
|
||||
func libraryItemsPublisher(
|
||||
func fetchLinkedItems(
|
||||
limit: Int,
|
||||
sortDescending: Bool,
|
||||
searchQuery: String?,
|
||||
cursor: String?
|
||||
) -> AnyPublisher<HomeFeedData, ServerError> {
|
||||
// Attempt to sync items that have unsynched items
|
||||
Task {
|
||||
try? await syncOfflineItemsWithServerIfNeeded()
|
||||
}
|
||||
) async throws -> HomeFeedData {
|
||||
// Send offline changes to server before fetching items
|
||||
try? await syncOfflineItemsWithServerIfNeeded()
|
||||
|
||||
struct InternalHomeFeedData {
|
||||
let items: [InternalLinkedItem]
|
||||
@ -50,7 +46,7 @@ public extension DataService {
|
||||
sharedOnly: .present(false),
|
||||
sort: OptionalArgument(
|
||||
InputObjects.SortParams(
|
||||
order: .present(sortDescending ? .descending : .ascending),
|
||||
order: .present(.descending),
|
||||
by: .updatedTime
|
||||
)
|
||||
),
|
||||
@ -64,33 +60,29 @@ public extension DataService {
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
send(query, to: path, headers: headers) { result in
|
||||
switch result {
|
||||
case let .success(payload):
|
||||
switch payload.data {
|
||||
case let .success(result: result):
|
||||
if let items = result.items.persist(context: self.backgroundContext) {
|
||||
promise(.success(HomeFeedData(items: items.map(\.objectID), cursor: result.cursor)))
|
||||
} else {
|
||||
promise(.failure(.unknown))
|
||||
}
|
||||
case .error:
|
||||
promise(.failure(.unknown))
|
||||
}
|
||||
case .failure:
|
||||
promise(.failure(.unknown))
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(query, to: path, headers: headers) { [weak self] queryResult in
|
||||
guard let payload = try? queryResult.get() else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "network error"))
|
||||
return
|
||||
}
|
||||
|
||||
switch payload.data {
|
||||
case let .success(result: result):
|
||||
if let context = self?.backgroundContext, let items = result.items.persist(context: context) {
|
||||
continuation.resume(returning: HomeFeedData(items: items.map(\.objectID), cursor: result.cursor))
|
||||
} else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
|
||||
}
|
||||
case .error:
|
||||
continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
let homeFeedItemSelection = Selection.Article {
|
||||
private let articleSelection = Selection.Article {
|
||||
InternalLinkedItem(
|
||||
id: try $0.id(),
|
||||
title: try $0.title(),
|
||||
@ -114,5 +106,5 @@ let homeFeedItemSelection = Selection.Article {
|
||||
}
|
||||
|
||||
private let articleEdgeSelection = Selection.ArticleEdge {
|
||||
try $0.node(selection: homeFeedItemSelection)
|
||||
try $0.node(selection: articleSelection)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user