All skills

Code generator skills that produce production-ready Swift code for common app components. Use when user wants to add logging, analytics, onboarding, review prompts, networking, authentication, paywalls, settings, persistence, error monitoring, CI/CD pipelines, localization, push notifications, deep linking, testing, accessibility, widgets, feature flags, app icons, image caching, pagination, HTTP caching, share cards, social export, subscription lifecycle, referral systems, watermarks, streak tracking, milestone celebrations, what's new screens, lapsed user re-engagement, usage insights, variable rewards, consent flows, account deletion, permission priming, force updates, state restoration, debug menus, offline queues, feedback forms, announcement banners, quick win sessions, Spotlight indexing, App Clips, screenshot automation, background processing, app extensions, data export, or SwiftUI preview sample data and variant matrices.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/generators

This session only. Nothing lands on disk.

paginationpagination-patterns.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Pagination Patterns and Best Practices

Offset vs Cursor Pagination

Offset-Based

GET /users?page=2&size=20

Response:

{
  "items": [...],
  "totalItems": 150,
  "totalPages": 8,
  "currentPage": 2,
  "pageSize": 20
}
Pros Cons
Simple to implement Inconsistent on inserts/deletes
Jump to any page Slow on large datasets (SQL OFFSET)
Know total count Duplicate/missing items when data changes
Easy "Page X of Y" UI

Best for: Admin dashboards, search results, stable datasets.

Cursor-Based

GET /users?cursor=eyJpZCI6MTAwfQ&limit=20

Response:

{
  "items": [...],
  "nextCursor": "eyJpZCI6MTIwfQ",
  "hasMore": true
}
Pros Cons
Consistent with real-time data Can't jump to arbitrary page
Performant at any depth No total count (usually)
No duplicates/gaps Opaque cursor (can't inspect)
Works with infinite scroll

Best for: Social feeds, chat, real-time data, large datasets.

Decision Guide

Is data frequently inserted/deleted?
├── Yes → Cursor-based
└── No
    ├── Need "jump to page N"? → Offset-based
    └── Infinite scroll only? → Either works, cursor preferred

State Machine

Pagination has well-defined states. Model them explicitly:

                    ┌─────────┐
                    │  idle    │  (initial state)
                    └────┬────┘
                         │ loadFirstPage()
                    ┌────▼────┐
              ┌─────│ loading  │─────┐
              │     └─────────┘     │
          success              failure
              │                     │
        ┌─────▼─────┐      ┌───────▼──────┐
        │  loaded    │      │    error      │
        │ (has more) │      │ (retryable)   │
        └─────┬─────┘      └───────┬───────┘
              │ loadNextPage()     │ retry()
        ┌─────▼─────┐             │
        │  loading   │◄────────────┘
        │  (next)    │
        └─────┬─────┘
              │
        ┌─────▼──────┐
        │  exhausted  │  (no more pages)
        └────────────┘

State Enum

enum PaginationState: Equatable {
    case idle
    case loading
    case loadingMore      // Loading subsequent pages (items already visible)
    case loaded
    case error(String)
    case exhausted        // All pages loaded, no more data
}

Key State Rules

  1. idle → loading: Only on first load
  2. loaded → loadingMore: On subsequent pages (keeps existing items visible)
  3. error → loading/loadingMore: On retry
  4. loaded → exhausted: When response has fewer items than page size or hasMore == false
  5. Any → idle: On refresh/reset

Why "loadingMore" is Separate from "loading"

  • loading: Show full-screen loading spinner, no items visible
  • loadingMore: Show items + small bottom spinner, user can still scroll

Threshold Prefetching

The Problem

If you wait until the user scrolls to the very bottom to load the next page, they see a loading spinner every time. Bad UX.

The Solution: Prefetch Threshold

Load the next page when the user is N items from the bottom:

func onItemAppear(_ item: Item) {
    let threshold = 5  // Load when 5 items from bottom
    guard let index = items.firstIndex(where: { $0.id == item.id }),
          index >= items.count - threshold else {
        return
    }
    Task { await loadNextPage() }
}

Choosing a Threshold

Page Size Threshold Why
10 3 Small pages load fast
20 5 Standard
50 10 Large pages need more lead time

Rule of thumb: threshold = pageSize / 4, minimum 3.

Pull-to-Refresh

Reset Everything on Refresh

func refresh() async {
    items.removeAll()
    currentPage = 0
    cursor = nil
    state = .idle
    await loadFirstPage()
}

SwiftUI Integration

List {
    // ... items
}
.refreshable {
    await manager.refresh()
}

Search + Pagination

The Challenge

Search queries change pagination context:

  • New query → reset to page 1
  • Same query, scroll → load next page
  • Empty query → show all (or clear)

Debouncing

Don't fire API request on every keystroke:

@Observable
final class SearchablePaginationManager<Source: PaginatedDataSource> {
    var query: String = "" {
        didSet {
            debounceTask?.cancel()
            debounceTask = Task {
                try await Task.sleep(for: .milliseconds(300))
                await resetAndSearch()
            }
        }
    }

    private var debounceTask: Task<Void, Error>?

    private func resetAndSearch() async {
        pagination.reset()
        await pagination.loadFirstPage()
    }
}

Cancel Previous Requests

When query changes, cancel in-flight requests:

func loadFirstPage() async {
    currentTask?.cancel()
    currentTask = Task {
        try Task.checkCancellation()
        let response = try await dataSource.fetch(query: query, page: firstPage)
        try Task.checkCancellation()  // Check again before updating UI
        self.items = response.items
    }
    try? await currentTask?.value
}

Empty States

What to Show

State What to Show
idle Nothing (or skeleton)
loading (first page) Full-screen spinner or skeleton
loadingMore Items + bottom spinner
loaded (0 items) Empty state illustration
error (first page) Full-screen error with retry
error (next page) Items + error banner + retry
exhausted Items + "No more results" footer

ContentUnavailableView (iOS 17+)

if manager.items.isEmpty && manager.state == .loaded {
    ContentUnavailableView(
        "No Results",
        systemImage: "magnifyingglass",
        description: Text("Try a different search term")
    )
}

Error Handling

Retryable vs Non-Retryable

enum PaginationError: Error {
    case networkError(Error)    // Retryable
    case decodingError(Error)   // Usually not retryable
    case cancelled              // Not an error, ignore

    var isRetryable: Bool {
        switch self {
        case .networkError: return true
        case .decodingError: return false
        case .cancelled: return false
        }
    }
}

Don't Lose Items on Error

// ❌ Wrong: clear items on next-page error
func loadNextPage() async {
    do {
        let response = try await fetch()
        items = response.items  // Replaces existing!
    } catch {
        items = []  // User loses everything
    }
}

// ✅ Right: keep existing items, show error for retry
func loadNextPage() async {
    do {
        let response = try await fetch()
        items.append(contentsOf: response.items)  // Append
    } catch {
        state = .error(error.localizedDescription)  // Items preserved
    }
}

Performance Considerations

Diffable Data

Use Identifiable items for efficient SwiftUI diffing:

protocol PaginatedDataSource {
    associatedtype Item: Identifiable & Sendable
    // ...
}

Avoid Duplicate Requests

Guard against concurrent page loads:

func loadNextPage() async {
    guard state == .loaded else { return }  // Not idle, loading, or exhausted
    state = .loadingMore
    // ... fetch
}

Memory Management for Large Lists

For lists with thousands of items, consider:

  • LazyVStack (not List) for better memory behavior
  • Limit in-memory items and reload from cache
  • Use .onDisappear to release image data

Anti-Patterns to Avoid

Don't Use Array Index as Page Number

// ❌ Fragile — breaks if items are filtered or reordered
let page = items.count / pageSize

// ✅ Track page number explicitly
var currentPage = 0

Don't Ignore Task Cancellation

// ❌ Stale response overwrites newer data
let response = try await fetch(page: 2)
self.items = response.items  // Page 3 response may have already arrived

// ✅ Check cancellation
let response = try await fetch(page: 2)
try Task.checkCancellation()
self.items.append(contentsOf: response.items)

Don't Paginate on the Main Actor

// ❌ Blocks UI during fetch
@MainActor func loadNextPage() async {
    let data = try await URLSession.shared.data(from: url)  // Blocks UI
}

// ✅ Fetch off main, update on main
func loadNextPage() async {
    let response = try await dataSource.fetch(page: nextPage)
    await MainActor.run { items.append(contentsOf: response.items) }
}

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The 'generators' skill provides a robust set of templates and workflows for generating Swift code for iOS and macOS applications. Security analysis identified a surface for indirect prompt injection because the skill reads local project files to adapt its generation logic. It also utilizes dynamic execution by generating and running local Swift scripts to create application icons and uses shell commands to process images and audit project code. All external references are to well-known, established services.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • Runlayer7mo

    59/179 files flagged

  • ZeroLeaks5mo

    Scan incomplete

Signed by skilld at 30e898f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 2 months ago
What it can do
Reads files Edits files Runs commands
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 7 allowed tools
ReadWriteEditGlobGrepBashAskUserQuestion

README badge

README badge for rshankras/claude-code-apple-skills/generators