rshankras/pagination
Generates pagination infrastructure with offset or cursor-based patterns, infinite scroll, and search support. Use when user wants to add paginated lists, infinite scrolling, or load-more functionality.
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill pagination
Generate production pagination infrastructure supporting offset-based and cursor-based APIs, with infinite scroll SwiftUI views, state machine management, and optional search integration.
Use this skill when the user:
Search for existing networking code:
Glob: **/*API*.swift, **/*Client*.swift, **/*Endpoint*.swift
Grep: "APIClient" or "APIEndpoint"
If networking-layer generator was used, detect the APIEndpoint protocol and generate data sources that conform to it.
Search for existing pagination:
Glob: **/*Pagina*.swift, **/*LoadMore*.swift
Grep: "PaginationState" or "loadNextPage" or "hasMorePages"
If found, ask user whether to replace or extend.
Ask user via AskUserQuestion:
Read pagination-patterns.md for architecture guidance.
Read templates.md for production Swift code.
Generate these files:
PaginatedResponse.swift — Generic response models for offset and cursorPaginationState.swift — State machine (idle, loading, loaded, error, exhausted)PaginatedDataSource.swift — Protocol endpoints conform toPaginationManager.swift — @Observable manager with state transitionsBased on configuration:
SearchablePaginationManager.swift — If search selectedViews/PaginatedList.swift — Infinite scroll SwiftUI wrapperViews/LoadMoreButton.swift — Manual load-more buttonViews/PaginationStateView.swift — Empty/loading/error state viewsCheck project structure:
Sources/ exists → Sources/Pagination/App/ exists → App/Pagination/Pagination/After generation, provide:
Pagination/
├── PaginatedResponse.swift # Generic response models
├── PaginationState.swift # State machine enum
├── PaginatedDataSource.swift # Data source protocol
├── PaginationManager.swift # @Observable manager
├── SearchablePaginationManager.swift # Optional: search + pagination
└── Views/
├── PaginatedList.swift # Infinite scroll wrapper
├── LoadMoreButton.swift # Manual load-more
└── PaginationStateView.swift # Empty/loading/error states
Define a data source:
struct UsersDataSource: PaginatedDataSource {
typealias Item = User
let apiClient: APIClient
func fetch(page: PageRequest) async throws -> PaginatedResponse<User> {
try await apiClient.request(UsersEndpoint(page: page.page, size: page.size))
}
}
Use PaginationManager in a view model:
@Observable
final class UsersViewModel {
let pagination: PaginationManager<UsersDataSource>
init(apiClient: APIClient) {
pagination = PaginationManager(
dataSource: UsersDataSource(apiClient: apiClient)
)
}
}
With SwiftUI (infinite scroll):
struct UsersListView: View {
@State private var viewModel = UsersViewModel()
var body: some View {
PaginatedList(manager: viewModel.pagination) { user in
UserRow(user: user)
}
.task {
await viewModel.pagination.loadFirstPage()
}
}
}
With search:
struct SearchableUsersView: View {
@State private var searchManager = SearchablePaginationManager(
dataSource: UsersDataSource()
)
var body: some View {
PaginatedList(manager: searchManager.pagination) { user in
UserRow(user: user)
}
.searchable(text: $searchManager.query)
}
}
@Test
func loadFirstPagePopulatesItems() async throws {
let mockSource = MockDataSource(items: User.mockList(count: 20))
let manager = PaginationManager(dataSource: mockSource, pageSize: 10)
await manager.loadFirstPage()
#expect(manager.items.count == 10)
#expect(manager.state == .loaded)
#expect(manager.hasMore == true)
}
@Test
func loadAllPagesReachesExhausted() async throws {
let mockSource = MockDataSource(items: User.mockList(count: 15))
let manager = PaginationManager(dataSource: mockSource, pageSize: 10)
await manager.loadFirstPage()
await manager.loadNextPage()
#expect(manager.items.count == 15)
#expect(manager.state == .exhausted)
}
generators/networking-layer — Base networking layer for data sourcesgenerators/http-cache — Cache paginated responsesTake rshankras/pagination from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.