-
Notifications
You must be signed in to change notification settings - Fork 734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ApolloPagination's loadNext throwing loadInProgress
incorrectly
#3349
Comments
thanks for reporting! I'll take a look at this momentarily. |
Here's a demo app to repro this if that helps: https://github.com/mapy1874/loadInProgressBugDemo |
That's super helpful! Thank you! |
@mapy1874 I think I see the issue. If I'm understanding this correctly, you'd like to know whether or not the pager is currently fetching in order to show a loading cell/spinner at the tail of the list. In your function where you receive data from the pager: Since you're using the I have a few questions that can help in improving this API:
|
@Iron-Ham Thanks for your quick response! For the need, I guess it's less about the loading indicator. The bug was that no pagination would happen after we get the The assumption was that when we assign the new list and rendering the items on view, the library should have already done loading. Thus when the last item appears on the view, we are safe to trigger For your questions:
I do see people could get confused by this given it's natural to assume when
We started using ApolloPagination months ago, and there was no documentations back then and it seems natural for us to start with GraphQLQueryPager. Also, there are not that many materials on how to use |
Totally, that makes sense to me. Adding additional docs around
I think what's hard about this assumption is that it's not universally true. Imagine that you were fetching with
I'd have to think about how best to improve this – whether that's documentation or API. 🤔 |
Thanks! May I get more context on why we choose to throw a PaginationError in when The current way of throwing an error makes it challenging to work with the "onAppear of last item, try Also let me know if you think there could be some other ways handling this scenario! |
Before diving in – I think that this change should allow you to set your
I intentionally didn't allow for parallel execution for a few reasons:
Queued fetches weren't allowed for similar reasons:
Effectively, what this translates into is an intentional design decision that it's up to the caller to ensure that they aren't calling In the context of GitHub, we do so by having an object which manages infinite-scroll behavior. At its core, it observes a list view's scroll position and triggers a load when we are within a certain distance of the end of the list (e.g., when we are within
I'll take some some time this week and think on improvements to SwiftUI with an I really appreciate the feedback on this, it's super valuable! |
@mapy1874 I took another look at your project – and realized that this could be rewritten in-place without relying on any changes in the pagination library: mapy1874/loadInProgressBugDemo@main...Iron-Ham:loadInProgressBugDemo:main To summarize the changes made:
My mental model of list view UIs
Everyone has a different mental model, and every application has unique constraints and requirements.
Let me know if that's helpful as a demonstration! |
Thanks for sharing! This does help solve the bug/error for this specific scenario. Without library change, I guess in the real world the following scenario would still break if we have a large enough paged size and trigger pagination at certain position. I made the following changes on top of your commit:
To reproduce the bug: Though no more Really appreciate the discussions! This really helps me understand the library more. Do you have any suggestions fore this case if we were not changing this |
I think the error being experienced at this point isn't to do with Stepping away from early invocation of You could build in the behavior you'd prefer by:
As far as invocating Ultimately, the question of what should trigger a load is somewhat philosophical. I'm of the opinion that it's a user-triggered event. I fall into the camp of thinking that a specific row of data appearing on-screen isn't a user-triggered event – but a side effect of the action the user took: scrolling. Given that, it makes sense to me that an infinite scroll load is triggered on scroll-based logic – especially given the default behaviors of the As an aside, I think I've got an idea for a small As a quick note on why we don't allow fetches on cached data, since I don't think I explained that in the previous message – we don't allow fetches on cached data, since cached data may be stale. This is a common bug for folks that use a |
Looks like it's better for us to adopt the scrolling scheme instead of relying on the library to adapt to this triggering loadMore Not sure what's a good way to point out that this loadMore Also love the mini package idea! Do let me know if you get to it someday. Thanks for all the valuable inputs! |
Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo iOS usage and allow us to serve you better. |
@mapy1874 As a quick aside – there should be a clean workaround for In the near future, your (changes are: addition of import Apollo
import ApolloPagination
import RocketReserverAPI
import SwiftUI
private let pageSize = 10
extension LaunchListQuery.Data.Launches.Launch: Identifiable { }
@Observable final class LaunchListViewModel: ObservableObject {
var showTailSpinner = false
var canLoadNext: Bool { pager.canLoadNext }
var launches: [LaunchListQuery.Data.Launches.Launch] = []
var hasFirstPageLoaded: Bool = false
var error: Error?
var showError: Bool {
get { error != nil }
set { error = nil }
}
private var pager: GraphQLQueryPager<[LaunchListQuery.Data.Launches.Launch]>
init() {
let initialQuery = LaunchListQuery(pageSize: .some(pageSize), cursor: .none)
self.pager = GraphQLQueryPager(
client: Network.shared.apollo,
initialQuery: initialQuery,
extractPageInfo: { data in
CursorBasedPagination.Forward(hasNext: data.launches.hasMore, endCursor: data.launches.cursor)
},
pageResolver: { page, direction in
LaunchListQuery(pageSize: .some(pageSize), cursor: page.endCursor ?? .none)
},
transform: { data in
data.launches.launches.compactMap { $0 }
}
)
pager.subscribe { result in
switch result {
case .success((let launches, _)):
self.launches = launches
case .failure(let error):
// These are network errors, and worth showing to the user.
self.error = error
}
}
fetch()
}
func refresh() {
pager.refetch()
}
func fetch() {
hasFirstPageLoaded = false
pager.fetch() {
hasFirstPageLoaded = true
}
}
func loadNextPage() {
guard canLoadNext, !showTailSpinner else { return }
self.showTailSpinner = true
pager.loadNext() { error in
self.showTailSpinner = false
// This is a usage error
if let error {
assertionFailure(error.localizedDescription)
}
}
}
} You could somewhat emulate this behavior today, by having the file look like this: (Changes are: Addition of import Apollo
import ApolloPagination
import RocketReserverAPI
import SwiftUI
private let pageSize = 10
extension LaunchListQuery.Data.Launches.Launch: Identifiable { }
@Observable final class LaunchListViewModel: ObservableObject {
var showTailSpinner = false
var canLoadNext: Bool { pager.canLoadNext }
var launches: [LaunchListQuery.Data.Launches.Launch] = []
var hasFirstPageLoaded: Bool = false
var error: Error?
var showError: Bool {
get { error != nil }
set { error = nil }
}
private var pager: GraphQLQueryPager<[LaunchListQuery.Data.Launches.Launch]>
init() {
let initialQuery = LaunchListQuery(pageSize: .some(pageSize), cursor: .none)
self.pager = GraphQLQueryPager(
client: Network.shared.apollo,
initialQuery: initialQuery,
extractPageInfo: { data in
CursorBasedPagination.Forward(hasNext: data.launches.hasMore, endCursor: data.launches.cursor)
},
pageResolver: { page, direction in
LaunchListQuery(pageSize: .some(pageSize), cursor: page.endCursor ?? .none)
},
transform: { data in
data.launches.launches.compactMap { $0 }
}
)
pager.subscribe { result in
switch result {
case .success((let launches, let source)):
self.launches = launches
if source == .fetch {
self.hasFirstPageLoaded = true
}
case .failure(let error):
// These are network errors, and worth showing to the user.
self.error = error
}
}
fetch()
}
func refresh() {
pager.refetch()
}
func fetch() {
hasFirstPageLoaded = false
pager.fetch()
}
func loadNextPage() {
guard canLoadNext, !showTailSpinner else { return }
self.showTailSpinner = true
pager.loadNext() { error in
self.showTailSpinner = false
// This is a usage error
if let error {
assertionFailure(error.localizedDescription)
}
}
}
} This would rely on a |
@mapy1874 I found this – which seems to be in spirit with what i would have built out as a package: https://github.com/danielsaidi/ScrollKit/blob/main/Sources/ScrollKit/Helpers/ScrollViewOffsetTracker.swift#L11-L35 I haven't used it, so I can't speak to how well it works – but it seems like what you're looking for |
Summary
I have a forward, offset-based list using the library. onAppear of the last item of the list, it will trigger
loadNext
to fetch more items. If I scroll the screen fast enough sometimes theloadNext
will not fetch more items and throw the error sinceisFetching
is true inpaginationFetch
. From a user perspective, this causes the pagination to stop.I believe the
defer { isFetching = false }
inpaginationFetch
is not executed promptly afterwatcher.refetch
finishes, as adding an additional line ofisFetching = false
here fix the issue I'm facing.Version
0.1.0
Steps to reproduce the behavior
See the summary
Logs
No response
Anything else?
No response
The text was updated successfully, but these errors were encountered: