관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] UITableView 하단 로딩 구현 방법 (#페이지네이션, footer loading, UIActivityIndicatorView) 본문

iOS 응용 (swift)

[iOS - swift] UITableView 하단 로딩 구현 방법 (#페이지네이션, footer loading, UIActivityIndicatorView)

jake-kim 2023. 10. 12. 01:07

구현된 하단 로딩

하단 로딩 구현 아이디어

  • 1단계) Pagination: tableView의 willDisplay 델리게이트에서 마지막 인덱스 값인지 체크하고, 마지막 인덱스 값이면 페이지네이션 구현
  • 2단계) 하단로딩: willDisplay에서 페이지네이션이 되기 전에 tableView의 footerView에 indicator가 있는 UITableViewHeaderFooterView를 대입해주고, 데이터가 들어오면 다시 footerView.tableFooterView를 nil로 초기화

하단 로딩 구현

  • pagination 형태 구현
class ViewController: UIViewController {
    private let tableView: UITableView = {
        let view = UITableView()
        view.contentInsetAdjustmentBehavior = .scrollableAxes
        view.allowsSelection = false
        view.backgroundColor = .clear
        view.separatorStyle = .none
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    private var dataSource = (1...10).map(String.init)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
        ])
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
    }
    
    private func loadMore() {
        let curCount = dataSource.count
        dataSource.append(contentsOf: (1+curCount...10+curCount).map(String.init))
    }
}

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

extension ViewController: UITableViewDelegate {
    func tableView(
        _ tableView: UITableView,
        willDisplay cell: UITableViewCell,
        forRowAt indexPath: IndexPath
    ) {
        let isLastCursor = indexPath.row == dataSource.count - 1
        guard isLastCursor else { return }
        loadMore()
        tableView.reloadData()
    }
}
  • 여기에 로딩을 넣어주어야 하므로 로딩 컴포넌트인 UIActivityIndicatorView를 가지고 있는 FooterView 구현
import UIKit

final class FooterView: UITableViewHeaderFooterView {
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        
        let indicatorView = UIActivityIndicatorView()
        indicatorView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(indicatorView)
        
        NSLayoutConstraint.activate([
            indicatorView.leadingAnchor.constraint(equalTo: leadingAnchor),
            indicatorView.trailingAnchor.constraint(equalTo: trailingAnchor),
            indicatorView.bottomAnchor.constraint(equalTo: bottomAnchor),
            indicatorView.topAnchor.constraint(equalTo: topAnchor),
        ])
        
        indicatorView.startAnimating()
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
}
  • tableView에 headerView 등록
tableView.register(FooterView.self, forHeaderFooterViewReuseIdentifier: "footer")
  • 델리게이트에서 headerView 높이 설정
extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        130
    }
}
  • willDisplay에서 로딩 footerView 적용
func tableView(
    _ tableView: UITableView,
    willDisplay cell: UITableViewCell,
    forRowAt indexPath: IndexPath
) {
    let isLastCursor = indexPath.row == dataSource.count - 1
    guard isLastCursor else { return }
    
    // 1. 로딩뷰 생성
    let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "footer") as? FooterView
    tableView.tableFooterView = footerView
    
    loadMore()
    tableView.reloadData()
    
    // 2. 로딩뷰 제거
    // 테스트를 위해 delay
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        tableView.tableFooterView = nil
    }
}

(완성)

구현된 하단 로딩

* 주의사항

  • 아래처럼 footerView가 셀 위쪽에 그려지는 버그가 있을 수 있는데 해결 방법은 tableView에 register로 headerView 등록을 안쓰면 해결이 가능

  • 아래처럼 footerView를 register하여 사용하면 헤더뷰의 frame height를 잘 못불러오는 케이스가 발생
tableView.register(FooterView.self, forHeaderFooterViewReuseIdentifier: "footer")
...
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "footer") as? FooterView
tableView.tableFooterView = footerView
  • register하는 코드를 지우고, FooterView 뷰를 직접 초기화할 때 frame에 height를 넣어주면 해결
tableView.tableFooterView = FooterView(frame: .init(x: 0, y: 0, width: 0, height: 72))

(완성)

 

* 전체 코드: https://github.com/JK0369/ExBottomLoading

* 참고

https://ios-development.tistory.com/682

Comments