Created
December 24, 2024 11:35
-
-
Save DominicHolmes/0b6c9b2615495b0f8a3b991dcd494328 to your computer and use it in GitHub Desktop.
Generic paginated ForEach component; given a fetch descriptor, continually fetch more pages of results as the user scrolls to the end of the existing results
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import SwiftData | |
struct PaginatedForEach<Content: View, Model: PersistentModel>: View { | |
@Environment(\.modelContext) private var context | |
@State private var results: [Model] = [] | |
@State private var currentPage: Int = 0 | |
let query: FetchDescriptor<Model> | |
let onUpdateCount: ((Int) -> Void)? | |
let content: (Model) -> Content | |
init(query: FetchDescriptor<Model>, | |
onUpdateCount: ((Int) -> Void)? = nil, | |
content: @escaping (Model) -> Content) { | |
self.query = query | |
self.onUpdateCount = onUpdateCount | |
self.content = content | |
} | |
/// How many results in each page, configured by the FetchDescriptor. Defaults to 20 | |
private var pageSize: Int { | |
query.fetchLimit ?? 20 | |
} | |
/// When the [results.count - marginToEnd] item appears, fetch more results | |
private var marginToEnd: Int { | |
max(0, pageSize - 10) | |
} | |
var body: some View { | |
if results.isEmpty { | |
// Unfortunately, onAppear is not called on an empty ForEach, so this is necessary for the first load | |
ProgressView().onAppear { fetch() } | |
} else { | |
ForEach(Array(results.enumerated()), id: \.element.id) { index, result in | |
content(result) | |
.onAppear { | |
fetchIfNecessary(at: index) | |
} | |
} | |
.animation(.default, value: results) | |
} | |
} | |
private func fetchIfNecessary(at index: Int) { | |
if index + marginToEnd == results.count { | |
Task(priority: .userInitiated) { | |
currentPage += 1 | |
fetch(currentPage) | |
} | |
} | |
} | |
private func fetch(_ page: Int = 0) { | |
var fetchDescriptor = query | |
fetchDescriptor.fetchLimit = pageSize | |
fetchDescriptor.fetchOffset = currentPage * pageSize | |
do { | |
results += try context.fetch(fetchDescriptor) | |
onUpdateCount?(results.count) | |
} catch { | |
print(error) | |
} | |
} | |
} | |
// MARK: - USAGE EXAMPLE BELOW | |
struct ActivityList: View { | |
@State private var resultsCount: Int = 0 | |
private var query: FetchDescriptor<Activity> { | |
var fetchDescriptor = FetchDescriptor<Activity>() | |
fetchDescriptor.fetchLimit = 20 | |
fetchDescriptor.sortBy = [.init(\.startDate, order: .reverse)] | |
return fetchDescriptor | |
} | |
private func onUpdateResultCount(_ count: Int) { | |
resultsCount = count | |
} | |
var body: some View { | |
List { | |
Section { | |
PaginatedForEach(query: query, onUpdateCount: onUpdateResultCount) { activity in | |
NavigationLink(value: activity) { | |
ActivityListRow(activity: activity) | |
} | |
.buttonStyle(PlainButtonStyle()) | |
} | |
} | |
Section { | |
Text("\(resultsCount) total") | |
.multilineTextAlignment(.center) | |
.frame(maxWidth: .infinity) | |
} | |
.listRowSeparator(.hidden) | |
} | |
.listStyle(PlainListStyle()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment