스위프트 테이블 뷰 무한 스크롤링 구현하기

개발자의 일상, @munondio 팔로우 해보세요!

최근 조그맣게 시작한 개인 프로젝트가 있습니다. 무엇인가를 키우는 사람을 위한 SNS iOS 앱을 만드는 프로젝트인데요. 아직 기획도 채 끝나지 않은 초기 단계에 있는 프로젝트이지만, 프로젝트를 진행하면서 사용한 기술들을 남겨보고자 합니다.

SNS의 최고 미덕은 '끊임없이 스크롤 되는 피드'라고 생각합니다. 그래서 기초적인 디자인을 배치한 뒤, 바로 무한 스크롤부터 구현하기 시작했습니다. 이 무한 스크롤은 결국 흔히 '피드'라고 불리는 부분에 사용될 것인데요. 이것을 구현하기 위해 테이블 뷰를 사용할지, 아니면 컬렉션 뷰를 사용할지 고민을 많이 했습니다.

하지만 개인 프로젝트인 만큼, 잘못되면 나중에 고치자는 생각으로 조금 더 간단한 테이블 뷰를 사용해서 구현하기 시작했습니다. 테이블 뷰를 오토 레이아웃으로 정렬한 뒤, 테이블 뷰 셀을 테이블 뷰에 넣었습니다. (사실 이 글을 작성할 때는 뒤의 기능들까지 구현한 상태여서 추가적인 테이블 뷰 셀이 존재합니다.)

그리고 이 테이블 뷰 셀을 갖고 프로그래밍적으로 구현하기 시작합니다. 아참! 그 전에, 해당 뷰 컨트롤러에 커스텀 클래스를 연결해주고, 테이블 뷰를 dataSource와 delegate 연결해줍니다.

swift-infinite-scrolling

swift-infinite-scrolling

그러면 기본 준비는 다 되었습니다. 이제 다음과 같이 코드를 구현 해줍니다.

import UIKit
    
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate
{
    @IBOutlet weak var tableView: UITableView!
    
    var fetchingMore = false
    var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        let loadingNib = UINib(nibName: "LoadingCell", bundle: nil)
        tableView.register(loadingNib, forCellReuseIdentifier: "loadingCell")
    }
    
    func numberOfSections(in tableView: UITableView) -> Int
    {
        return 3
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        if section == 0
        {
            return 1
        } else if section == 1
        {
            return items.count
        } else if section == 2 && fetchingMore
        {
            return 1
        }
        
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        if indexPath.section == 0
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: "storyCell", for: indexPath)
            
            return cell            
        } else if indexPath.section == 1
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! FeedTableViewCell
            cell.username.setTitle("User \(items[indexPath.row])", for: .normal)
            
            return cell
        } else
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingCell
            cell.spinner.startAnimating()
            
            return cell
        }
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView)
    {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        
        if offsetY > contentHeight - scrollView.frame.height
        {
            if !fetchingMore
            {
                beginBatchFetch()
            }
        }
    }
    
    func beginBatchFetch()
    {
        fetchingMore = true
        tableView.reloadSections(IndexSet(integer: 2), with: .none)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
            let newItems = (self.items.count...self.items.count + 10).map { index in index }
            self.items.append(contentsOf: newItems)
            self.fetchingMore = false
            self.tableView.reloadData()
        })
    }    
}

이제, 부분적으로 살펴보면

import UIKit
    
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate
{
    @IBOutlet weak var tableView: UITableView!
    
    var fetchingMore = false
    var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        let loadingNib = UINib(nibName: "LoadingCell", bundle: nil)
        tableView.register(loadingNib, forCellReuseIdentifier: "loadingCell")
    }

이 부분은 피드 페이지를 위한 뷰 컨트롤러 HomeViewController의 시작 부분입니다. HomeViewController 클래스를 만들고, 기본적으로 UIViewController를 상속 시켜줍니다. 그리고 테이블 뷰를 위한 클래스들을 상속 시켜줍니다. UITableViewDataSource, UITableViewDelegate를 말입니다.

그리고 나서는 tableView라는 UITableView형의 IBOoutlet 변수를 만들어주고, fetchingMore 변수와 items 배열을 생성했습니다. viewDidLoad는 뷰가 화면에 로드 되면 호출되는 함수인데요. 뷰 로드를 하면서 loadingCell을 생성하기 위해 저렇게 구현했습니다.

    func numberOfSections(in tableView: UITableView) -> Int
    {
        return 3
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        if section == 0
        {
            return 1
        } else if section == 1
        {
            return items.count
        } else if section == 2 && fetchingMore
        {
            return 1
        }
        
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        if indexPath.section == 0
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: "storyCell", for: indexPath)
            
            return cell            
        } else if indexPath.section == 1
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! FeedTableViewCell
            cell.username.setTitle("User \(items[indexPath.row])", for: .normal)
            
            return cell
        } else
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingCell
            cell.spinner.startAnimating()
            
            return cell
        }
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView)
    {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        
        if offsetY > contentHeight - scrollView.frame.height
        {
            if !fetchingMore
            {
                beginBatchFetch()
            }
        }
    }
    
    func beginBatchFetch()
    {
        fetchingMore = true
        tableView.reloadSections(IndexSet(integer: 2), with: .none)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
            let newItems = (self.items.count...self.items.count + 10).map { index in index }
            self.items.append(contentsOf: newItems)
            self.fetchingMore = false
            self.tableView.reloadData()
        })
    }

이 부분들은 tableView의 delegate 메소드들입니다.