리스트를 구현하다보면 아래 영상과 같이 페이지네이션을 해야할 때 가 많다(거의 다 라고 해야하나..;;)
아무튼 앱 개발 공부를 처음 할 때(부트캠프 할 때)는 새로운 배열이 들어올 때 마다 tableView(혹은 CollectionView)를 리로드 해버렸는데
이렇게 하면 이미지뷰에 이미지 할당 할 때 겁나 깜빡깜빡 거려서 메모리고 뭐고를 떠나서 꼴 보기가 싫었다...
그래서 페이지네이션 할 때는 추가되는 index에 해당하는 cell indexpath 만 append 해주고 해당 cell만 리로드 해주는 방식으로 구현하며 그 내용을 정리하려고 한다
ViewModel에서의 코드
private var listPageNumber = 0
private var listPageSize = 10
//MARK: - Output
var listHasNext = true //View에서 참고할 프로퍼티
private func getSomeList() {
isLoading.accept(true)
Task {
do {
let entity = try await getListUseCase.execute(pageNumber: listPageNumber,
pageSize: listPageSize)
await MainActor.run {
//새 리스트를 받아오면 해당 리스트를 뷰로 전달한다
appenList.accept(entity.items)
isLoading.accept(false)
//리스트를 뷰로 전달한 뒤 hasNext 및 pageNumber 수정
let lastPage = entity.totalCount / listPageSize
if lastPage == listPageNumber {
listHasNext = false
} else {
listPageNumber += 1
}
}
} catch let error {
await MainActor.run {
showAlert.accept(error.errorMessage)
isLoading.accept(false)
}
}
}
}
View에서의 코드
- view프로퍼티 세팅
private weak var viewModel: NewReviewTabVMable?
private var surveyList = [Entity.Item]()
//뷰에 처음 진입하면 바로 호출을 하는데 페이지 뷰는 끝에 닿아있어 두번 호출됨을 방지하기 위해 true로 생성
private var isPaging = true
- TableView 세팅
private lazy var listTV: UITableView = {
let tableView = UITableView()
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
//아이템을 표시할 cell과, 로딩 될 때 하단에 표시할 loadingCell 두 개 추가
tableView.register(ListTVCell.self, forCellReuseIdentifier: "\(ListTVCell.self)")
tableView.register(LoadingTVCell.self, forCellReuseIdentifier: "\(LoadingTVCell.self)")
return tableView
}()
extension TableViewListView: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
//0번재 section은 아이템 list를, 1번째 section은 loadingCell을 보여주기 위해 두 개 return
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//section이 0일 때는 아이템 개수 만큼 return
if section == 0 {
return list.count
}
//section이 1이고 페이징 중이고 다음 페이지가 있을 때 1번 section에 1개의 cell return
if section == 1 && isPaging && viewModel?.listHasNext == true {
return 1
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//section이 0일 때는 아이템 cell return
if indexPath.section == 0 {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(ListTVCell.self)", for: indexPath) as? ListTVCell,
let item = self.list[safe: indexPath.row] else {
return UITableViewCell()
}
cell.setCellContents(item: item)
return cell
}
//section이 1일 때는 loadingCell return
if indexPath.section == 1 {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(LoadingTVCell.self)", for: indexPath) as? LoadingTVCell else {
return UITableViewCell()
}
cell.startLoading()
return cell
}
return UITableViewCell()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let height = scrollView.frame.height
if offsetY > (contentHeight - height) { //table view가 바닥에 맞닿았을 때
if !isPaging && viewModel?.surveyListHasNext == true { //페이징 중이 아니고 다음 페이지가 있다면
isPaging = true //페이징 중으로 변경하고
surveyListTV.reloadSections(IndexSet(integer: 1), with: .none) //로딩 셀을 노출하기 위해 section reload를 하고
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.viewModel?.getList(listCase: .survey) //리스트 호출
}
}
}
}
}
여기서 0.5초 텀을 준 이유는 정확히 원인은 모르겠는데 간혹가다가 두 번 호출 되면서 1번 loading section에 item이 할당되면서 이전 row가 업데이트가 안됐는데 아이템 수 만큼 update하려고 한다면서 죽는 현상이 발생해 안정적으로 0.5초 뒤에 호출 하도록 함
- cell 반영 하는 코드
func appendList(newList: [Entity.Item]) {
let appendedCount = self.list.count //새 아이템 추가 전 count 기억
self.list.append(contentsOf: newList) //새 아이템 append
//만약 새 아이템 추가 전 list가 비어있으면 그냥 reload, 아니라면 else구문 실행
if appendedCount == 0 {
listTV.reloadData()
isPaging = false
} else {
var indexPathes = [IndexPath]()
//appendedCount에 해당하는 index ~ appendedCount + 새로운 아이템 count 미만까지 추가
for index in appendedCount..<(appendedCount + newList.count) {
indexPathes.append(IndexPath(row: index, section: 0))
}
위 indexPathes배열에 해당하는 cell들을 추가하고 리로드
UIView.performWithoutAnimation {
listTV.insertRows(at: indexPathes, with: .none)
listTV.reloadRows(at: indexPathes, with: .none)
}
isPaging = false
1번째 section reload(로딩이 끝나 로딩 cell을 숨기기 위함)
listTV.reloadSections(IndexSet(integer: 1), with: .none)
}
}
이게 최고의 방법이라고 할 수는 없겠지만
지금까지 페이지네이션을 구현할 때마다 코드도 다르고 정리도 안돼서 수정하거나 어디서 문제가 되는지 찾아보기가 너무 힘들었는데 계속 구현하다보니 이제는 어느정도 정리 된 듯 싶어 앞으로 구현할 때 참고도 할 겸 글을 작성한다
'Swift > UIKit' 카테고리의 다른 글
높이 조절 가능한 CustomModal(Half Modal) (1) | 2024.02.13 |
---|---|
높이가 다른 이미지 다운로드 후 Cell에 할당 (0) | 2024.01.09 |
스크롤 중 타이머로 인한 뷰 업데이트가 멈추는 현상 (1) | 2024.01.03 |
Compositional Layout으로 여러가지 모양(?) 적용기 (1) | 2023.12.27 |