Mastering Elegant Pagination in SwiftUI Using Modern Swift Concurrency
Infinite scrolling is a staple of modern mobile applications, but implementing it correctly has historically been a headache. With the…
Mastering Elegant Pagination in SwiftUI Using Modern Swift Concurrency
Infinite scrolling is a staple of modern mobile applications, but implementing it correctly has historically been a headache. With the enforcement of Swift 6 Strict Concurrency Checking, the old workarounds utilizing @Published wrappers and loosely managed onAppear closures are no longer acceptable.
Today, we will walk through a robust, thread-safe, and elegant pagination implementation utilizing @Observable, @MainActor, and structured tasks.
The Modern Concurrency Paradigm
In the current ecosystem, avoiding data races is a compiler requirement, not just a suggestion. To achieve smooth pagination, we must ensure two things:
- Background Execution: The actual data fetching must not block the UI.
- Main Thread Mutations: Updating the list array must happen strictly on the main thread.
Step 1: The @Observable View Model
By marking our class with @Observable and @MainActor, we ensure our UI reacts instantly to changes and all properties are isolated safely.
import SwiftUI
@MainActor
@Observable
final class PaginationViewModel {
private(set) var items: [String] = []
private(set) var isLoading = false
private var currentPage = 1
private(set) var canLoadMore = true
func loadNextPage() async {
// Prevent duplicate concurrent requests
guard !isLoading && canLoadMore else { return }
isLoading = true
// Ensure isLoading is reset regardless of success or failure
defer { isLoading = false }
do {
let newItems = try await fetchItems(page: currentPage)
if newItems.isEmpty {
canLoadMore = false
} else {
items.append(contentsOf: newItems)
currentPage += 1
}
} catch {
print("Pagination error: \(error)")
}
}
// Mock network call
private func fetchItems(page: Int) async throws -> [String] {
try await Task.sleep(for: .seconds(1))
return (1...20).map { "Item \($0 + (page - 1) * 20)" }
}
}
Step 2: The View Implementation
SwiftUI’s .task modifier is the hero here. By attaching it to a ProgressView at the bottom of our list, it triggers the fetch operation only when the user scrolls near the end. Furthermore, .task is bound to the view's lifecycle—if the user navigates away, the network request is automatically cancelled.
struct PaginatedListView: View {
@State private var viewModel = PaginationViewModel()
var body: some View {
NavigationStack {
List {
ForEach(viewModel.items, id: \.self) { item in
Text(item)
.padding(.vertical, 8)
}
// Pagination Trigger
if viewModel.canLoadMore {
ProgressView()
.frame(maxWidth: .infinity)
.padding()
.task {
await viewModel.loadNextPage()
}
}
}
.navigationTitle("Modern Pagination")
// Initial Data Load
.task {
if viewModel.items.isEmpty {
await viewModel.loadNextPage()
}
}
}
}
}
Open to New Opportunities
I am an experienced Senior iOS Developer actively looking for a new role in a product team. I specialize in building highly scalable, beautifully animated applications utilizing modern Swift architectures.
- Connect with me on LinkedIn: https://www.linkedin.com/in/kanstantsin-klinau/
- If you’re serious about staying ahead, come join the stream on Telegram 🔗 https://t.me/tech_stream_actual_updates
메타데이터
- post_id
- 68ebbca7e853
- slug
- mastering-elegant-pagination-in-swiftui-using-modern-swift-concurrency-68ebbca7e853
- url
- https://medium.com/@kost9klinov/mastering-elegant-pagination-in-swiftui-using-modern-swift-concurrency-68ebbca7e853
- canonical_url
- https://medium.com/@kost9klinov/mastering-elegant-pagination-in-swiftui-using-modern-swift-concurrency-68ebbca7e853
- author_url
- https://medium.com/@kost9klinov
- status
- ok
- fetched_at
- 2026-07-10 08:54:07