convery items publisher to async func

This commit is contained in:
Satindar Dhillon
2022-04-26 08:53:55 -07:00
parent f51338083e
commit 93f11a4949
6 changed files with 80 additions and 88 deletions

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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?

View File

@ -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 }

View File

@ -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)
}