SwiftDataPager
is a Swift package designed to simplify the process of implementing pagination with SwiftData.
Working with large datasets in SwiftData
can be challenging when you want to load data incrementally. SwiftDataPager
provides an easy-to-use API that handles all the complexity of pagination, allowing developers to focus on creating great user experiences rather than managing fetch offsets and pagination state.
By providing property wrappers and view modifiers, SwiftDataPager
makes infinite scrolling and paginated data loading straightforward in SwiftUI applications.
Add SwiftDataPager
to your Swift project using Swift Package Manager.
dependencies: [
.package(url: "https://github.com/markbattistella/SwiftDataPager", from: "1.0.0")
]
@PagedQuery(fetchLimit: 20) var movies: [Movie]
@PagedQuery(
fetchLimit: 10,
sortDescriptors: [SortDescriptor(\Movie.releaseDate, order: .reverse)],
filterPredicate: #Predicate { $0.genre == "Action" },
logger: .default
) var actionMovies: [Movie]
SwiftDataPager
comes with several view modifiers to make pagination even easier:
Warning
The .onLoadMore(item:, in:)
is a required modifier on each cell item. This helps identify when we have reached the limit, and fetch new results. It has been optimised to exit early if not required to fetch.
ForEach(movies) { movie in
MovieRow(movie: movie)
.onLoadMore(item: movie, in: $movies)
}
Load earlier than the last item:
.onPaginationThreshold(threshold: 3, item: movie, in: $movies)
Use your own logic to trigger loadMore()
:
.onPaginationTrigger(item: movie, in: $movies) { current, all in
current.popularity > 8.0 && all.count > 10
}
Display a loading spinner during fetch:
if $movies.isFetching {
ProgressView()
.showFetching(in: $movies)
}
Great for empty states:
List {
// List content
}
.onEmptyLoad(in: $movies)
SwiftDataPager provides error state tracking to handle fetch failures gracefully:
if let error = $movies.error {
Text("Failed to load: \(error.localizedDescription)")
Button("Retry") { $movies.retry() }
}
You can reset pagination to start fresh:
Button("Reset") {
$movies.reset()
}
Toggle and customise logging to see what's going on:
@PagedQuery(fetchLimit: 20, logger: .default) var movies: [Movie]
Available logging options:
.none
: No logs.default
: Logs all entries from the wrapper to console.custom(MyCustomLogger())
: Provide your own logging system
Tip
You can use your own logging system so you can also send information to crash aggregators or telemetry systems besides logging only to the user's device.
import SwiftUI
import SwiftData
import SwiftDataPager
struct MovieListView: View {
@PagedQuery(
fetchLimit: 100,
sortDescriptors: [.init(\Movie.name)],
filterPredicate: #Predicate { $0.name.contains("AU") },
logger: .default
) private var movies: [Movie]
var body: some View {
NavigationStack {
List {
ForEach(movies) { movie in
Text(movie.name)
.onLoadMore(item: movie, in: $movies)
}
}
.navigationTitle("Movies")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
if $items.isFetching {
ProgressView()
.showFetching(in: $items)
}
}
ToolbarItem(placement: .topBarTrailing) {
if $items.hasReachedEnd {
Text("All done!")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
ToolbarItem(placement: .bottomBar) {
if let error = $items.error {
Text("Error: \(error.localizedDescription)")
.foregroundColor(.red)
}
}
}
}
}
}
Demo pagination of 10000
records.
SwiftDataPager-10000Records.mp4
Contributions are always welcome! Feel free to submit a pull request or open an issue for any suggestions or improvements you have.
SwiftDataPager
is licensed under the MIT License. See the LICENCE file for more details.