← Назад к вопросам
Как реализовать пагинацию (бесконечный скролл) в UITableView?
1.2 Junior🔥 31 комментариев
#CI/CD и инструменты разработки#Soft Skills и карьера#SwiftUI
Комментарии (1)
🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация пагинации в UITableView
Бесконечный скролл (пагинация) — это техника загрузки данных порциями по мере прокрутки пользователем таблицы. Я реализовывал её в десятках проектов, и вот отработанная архитектура:
Основные принципы реализации
Ключевые компоненты системы:
- Состояние загрузки (
isLoading,hasMoreData) - Детектирование приближения к концу через
UIScrollViewDelegate - Асинхронная загрузка данных с обработкой ошибок
- Индикатор активности внизу таблицы
Базовый менеджер пагинации
class PaginationManager<T> {
private(set) var items: [T] = []
private(set) var currentPage = 0
private(set) var isLoading = false
private(set) var hasMoreData = true
let pageSize: Int
init(pageSize: Int = 20) {
self.pageSize = pageSize
}
func loadNextPage(completion: @escaping (Result<[T], Error>) -> Void) {
guard !isLoading && hasMoreData else { return }
isLoading = true
currentPage += 1
// Здесь API запрос
fetchData(page: currentPage, pageSize: pageSize) { result in
self.isLoading = false
switch result {
case .success(let newItems):
self.items.append(contentsOf: newItems)
self.hasMoreData = newItems.count == self.pageSize
completion(.success(newItems))
case .failure(let error):
self.currentPage -= 1
completion(.failure(error))
}
}
}
func reset() {
items.removeAll()
currentPage = 0
isLoading = false
hasMoreData = true
}
private func fetchData(page: Int, pageSize: Int,
completion: @escaping (Result<[T], Error>) -> Void) {
// Реализация API-запроса
}
}
Интеграция с UITableView
class InfiniteScrollTableViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private let paginationManager = PaginationManager<YourModel>()
private let activityIndicator = UIActivityIndicatorView(style: .medium)
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
loadInitialData()
}
private func setupTableView() {
tableView.dataSource = self
tableView.delegate = self
tableView.tableFooterView = activityIndicator
// Регистрация ячеек
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
private func loadInitialData() {
paginationManager.reset()
loadMoreData()
}
private func loadMoreData() {
paginationManager.loadNextPage { [weak self] result in
guard let self = self else { return }
DispatchQueue.main.async {
self.activityIndicator.stopAnimating()
switch result {
case .success:
self.tableView.reloadData()
case .failure(let error):
self.showError(error)
}
}
}
}
}
UIScrollViewDelegate для детектирования прокрутки
extension InfiniteScrollTableViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let frameHeight = scrollView.frame.size.height
// Проверяем, доскроллили ли до 80% контента
if offsetY > contentHeight - frameHeight * 1.8 {
if !paginationManager.isLoading && paginationManager.hasMoreData {
activityIndicator.startAnimating()
loadMoreData()
}
}
}
}
UITableViewDataSource
extension InfiniteScrollTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
let count = paginationManager.items.count
// Добавляем одну ячейку для индикатора загрузки
if paginationManager.hasMoreData {
return count + 1
}
return count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Ячейка-индикатор загрузки
if indexPath.row == paginationManager.items.count {
let cell = tableView.dequeueReusableCell(
withIdentifier: "LoadingCell",
for: indexPath
)
cell.textLabel?.text = "Загрузка..."
cell.selectionStyle = .none
return cell
}
// Обычная ячейка с данными
let cell = tableView.dequeueReusableCell(
withIdentifier: "Cell",
for: indexPath
)
let item = paginationManager.items[indexPath.row]
cell.textLabel?.text = "Item \(indexPath.row)"
return cell
}
}
Продвинутые техники
- Prefetching API (iOS 10+):
extension InfiniteScrollTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
let needsLoad = indexPaths.contains { indexPath in
indexPath.row >= paginationManager.items.count - 5
}
if needsLoad && !paginationManager.isLoading && paginationManager.hasMoreData {
loadMoreData()
}
}
}
- Состояния загрузки:
enum LoadingState {
case initial
case loading
case loaded([YourModel])
case error(Error)
case noMoreData
}
- Оптимизация перезагрузки:
private func insertNewRows(_ newItems: [YourModel]) {
let startIndex = paginationManager.items.count - newItems.count
let endIndex = startIndex + newItems.count
let indexPaths = (startIndex..<endIndex).map {
IndexPath(row: $0, section: 0)
}
tableView.performBatchUpdates {
tableView.insertRows(at: indexPaths, with: .automatic)
}
}
Важные нюансы
- Race conditions: Всегда проверяйте
isLoadingперед началом новой загрузки - Retry логика: Добавляйте повторные попытки при сетевых ошибках
- Кеширование: Сохраняйте загруженные данные в CoreData/Realm для оффлайн-работы
- Отмена запросов: Используйте
URLSessionTaskдля возможности отмены - Pull-to-refresh: Реализуйте
UIRefreshControlдля обновления с первой страницы
Этот подход масштабируется для сложных сценариев, поддерживает множественные состояния интерфейса и легко тестируется. В production-проектах я обычно создаю отдельный слой PaginatedService с DI для лучшей архитектуры.