Swift UIKit 테이블 뷰 무한 스크롤링

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

무한 스크롤링

  • SNS의 최고 미덕은 '끊임없이 스크롤 되는 피드'라고 생각한다.
  • 그래서 기초적인 디자인을 배치한 뒤, 바로 무한 스크롤부터 구현하기 시작했다.
  • 이 무한 스크롤은 결국 흔히 '피드'라고 불리는 부분에 사용될 것이다.
  • 이것을 구현하기 위해 테이블 뷰를 사용할지, 아니면 컬렉션 뷰를 사용할지 고민을 많이 했다.
  • 하지만 개인 프로젝트인 만큼, 잘못되면 나중에 고치자는 생각으로
  • 조금 더 간단한 테이블 뷰를 사용해서 구현하기 시작했다.
  • 테이블 뷰를 오토 레이아웃으로 정렬한 뒤, 테이블 뷰 셀을 테이블 뷰에 넣었다.
  • 사실 이 글을 작성할 때는 뒤의 기능들까지 구현한 상태여서 추가적인 테이블 뷰 셀이 존재한다.

구현

  • 그리고 이 테이블 뷰 셀을 갖고 프로그래밍적으로 구현하기 시작한다.
  • 그 전에, 해당 뷰 컨트롤러에 커스텀 클래스를 연결해주고,
swift-infinite-scrolling
  • 테이블 뷰를 dataSource와 delegate 연결해준다.
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 메소드들이다.