Skip to content

Instantly share code, notes, and snippets.

@DominicHolmes
Created December 24, 2024 11:35
Show Gist options
  • Save DominicHolmes/0b6c9b2615495b0f8a3b991dcd494328 to your computer and use it in GitHub Desktop.
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
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