← Назад к вопросам

Как реализовать пагинацию (бесконечный скролл) в UITableView?

1.2 Junior🔥 31 комментариев
#CI/CD и инструменты разработки#Soft Skills и карьера#SwiftUI

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация пагинации в UITableView

Бесконечный скролл (пагинация) — это техника загрузки данных порциями по мере прокрутки пользователем таблицы. Я реализовывал её в десятках проектов, и вот отработанная архитектура:

Основные принципы реализации

Ключевые компоненты системы:

  1. Состояние загрузки (isLoading, hasMoreData)
  2. Детектирование приближения к концу через UIScrollViewDelegate
  3. Асинхронная загрузка данных с обработкой ошибок
  4. Индикатор активности внизу таблицы

Базовый менеджер пагинации

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
    }
}

Продвинутые техники

  1. 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()
        }
    }
}
  1. Состояния загрузки:
enum LoadingState {
    case initial
    case loading
    case loaded([YourModel])
    case error(Error)
    case noMoreData
}
  1. Оптимизация перезагрузки:
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 для лучшей архитектуры.