SwiftUI List with pull-to-refresh and infinite scroll

Sofia Martinez Jan 2026
2 tabs
import SwiftUI

struct PostsListView: View {
    @StateObject private var viewModel = PostsListViewModel()

    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.posts) { post in
                    NavigationLink {
                        PostDetailView(postId: post.id)
                    } label: {
                        PostRowView(post: post)
                            .onAppear {
                                if post.id == viewModel.posts.last?.id {
                                    viewModel.loadMoreIfNeeded()
                                }
                            }
                    }
                }

                if viewModel.isLoadingMore {
                    HStack {
                        Spacer()
                        ProgressView()
                        Spacer()
                    }
                }
            }
            .listStyle(.plain)
            .navigationTitle("Posts")
            .refreshable {
                await viewModel.refresh()
            }
            .overlay {
                if viewModel.isLoading && viewModel.posts.isEmpty {
                    ProgressView()
                } else if viewModel.posts.isEmpty {
                    ContentUnavailableView(
                        "No Posts",
                        systemImage: "doc.text",
                        description: Text("No posts available")
                    )
                }
            }
            .alert("Error", isPresented: $viewModel.showError) {
                Button("OK") { }
            } message: {
                Text(viewModel.errorMessage ?? "Unknown error")
            }
        }
        .task {
            await viewModel.loadInitialPosts()
        }
    }
}

struct PostRowView: View {
    let post: Post

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(post.title)
                .font(.headline)

            Text(post.body)
                .font(.subheadline)
                .foregroundColor(.secondary)
                .lineLimit(2)

            HStack {
                Label("\(post.likesCount)", systemImage: "heart")
                Label("\(post.commentsCount)", systemImage: "bubble.left")
                Spacer()
                Text(post.createdAt, style: .relative)
            }
            .font(.caption)
            .foregroundColor(.secondary)
        }
        .padding(.vertical, 4)
    }
}
2 files · swift Explain with highlit

Lists are fundamental to iOS apps, displaying scrollable collections efficiently. SwiftUI's List or ScrollView with LazyVStack renders items on-demand. I add pull-to-refresh with the .refreshable modifier, calling async refresh logic. For infinite scroll, I detect when the last item appears using onAppear and trigger pagination. The ViewModel tracks current page and hasMore state. Pagination prevents duplicate loads with loading flags. For performance with large datasets, I use LazyVStack instead of VStack to avoid rendering off-screen views. This pattern provides smooth, responsive lists that scale to thousands of items.