[ios] UITableView 무한스크롤 구현

Cobugi·2022년 2월 28일
1
post-thumbnail

문제

  • PunkAPI를 사용하여 맥주리스트를 만들고
  • 맥주의 상세정보를 표시하는 앱
  • 을 만들다가 무한스크롤을 구현하면서
  • 공부한 것을 적는다

설명

  • 데이터를 fetch하는 함수를 만들때

  • 현재 page를 매개변수로 받는다

  • 데이터 fetch를 다 했다면 completionHandler의 결과값을 매개변수로 전달할텐데

  • 이때 전달받은 현재 page를 +1 해서 매개변수로 돌려준다

  • 이를 받아 현재 page 값을 update하고

  • delegate 메소드 중 willDisplay에서

  • 현재 page가 1일 때를 제외하고

  • 이제 보여질 cell의 indexPath값에서 (indexPath.row + 1) / 25 + 1 가 currentPage와 같다면 다시 데이터를 fetch한 후 breweryList에 추가해주면된다

    왜 나누기 25냐면 PunkAPI가 기본적으로 25개씩 불러오기 때문! (이 개수는 쿼리로 설정할 수 있음)

    - ex) 처음 앱이 실행되었을 때 `currentPage`는 2 (한번 데이터를 fetch 하면 +1 되니까)
           스크롤 하다가 24번째(25개씩 데이터를 받아왔으니까 24번째가 마지막이다)가 보여질때
           (24 + 1) / 25 + 1 = 2 이므로 `currentPage`와 값이 같으므로
           다시 데이터를 fetch한다.

MORE

  • UITableViewDataSourcePrefetching의 메소드중 prefetchRowsAt를 사용하여 구현할 수도 있다고 하던데 나중에 구현해봐야겠다.

코드

import UIKit
import SnapKit

class InfiniteScrollViewController: UIViewController {
    
    private let fetchData = FetchData()
    private var breweryList = [Brewery]()
    private var currentPage = 1
    
    private lazy var tableView: UITableView = {
        let tableView = UITableView()
        
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        
        return tableView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLayout()
        fetchData.fetch(page: currentPage) { [weak self] breweryList, updatedPage in
            guard let self = self else { return }
            self.breweryList = breweryList
            self.currentPage = updatedPage
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
    }
}

extension InfiniteScrollViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        guard currentPage != 1 else { return }
        
        if (indexPath.row + 1) / 25 + 1 == currentPage {
            fetchData.fetch(page: currentPage) { [weak self] breweryList, updatedPage in
                guard let self = self else { return }
                self.breweryList.append(contentsOf: breweryList)
                self.currentPage = updatedPage
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
        }
    }
}

extension InfiniteScrollViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return breweryList.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = "\(breweryList[indexPath.row].id)_" + breweryList[indexPath.row].name
        return cell
    }
}

private extension InfiniteScrollViewController {
    func setupLayout() {
        view.addSubview(tableView)
        tableView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
}

// Model
struct Brewery: Decodable {
    let id: Int // 무한 스크롤 확인용
    let name: String
}

// FetchData
struct FetchData {
    func fetch(page: Int, completionHandler: @escaping ([Brewery], Int) -> Void) {
        guard var component = URLComponents(string: "https://api.punkapi.com/v2/beers") else { return }
        let queryItem = URLQueryItem(name: "page", value: "\(page)")
        component.queryItems = [queryItem]
        guard let url = component.url else { return }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard let response = response as? HTTPURLResponse else { return }
            switch response.statusCode {
            case (200..<300):
                guard let data = data else { return }
                
                do {
                    let result = try JSONDecoder().decode([Brewery].self, from: data)
                    completionHandler(result, page+1)
                } catch {
                    print("do-catch { \(error.localizedDescription) }")
                }
            default:
                guard let error = error else { return }
                print("statusCode error { \(error.localizedDescription) }")
            }
        }
        .resume()
    }
}
profile
iOS Developer 🐢

0개의 댓글