예전에도 Pagination은 해 본 경험이 몇 번 있었지만 뭔가 아직 개념이 명확하게 잡히지 않았다는 생각에 뚱땅뚱땅 Pagination 실습을 진행하게 되었다.
내가 정의한 기능은 다음과 같다. (출처 내 머리)
1. 텍스트 필트에 텍스트를 한 글자 입력할 때 마다 결과가 로드된다.
2. 입력 텍스트가 변경되면 데이터를 다시 받아온다.
3. UI는 image가 한줄에 3개씩, API 1회 호출 시 데이터가 20개씩 추가된다.
4. 가장 아래로 스크롤 하면 다음 결과를 불러온다.
5. 이미지는 캐싱처리되어 이후 필요할 경우 캐싱된 데이터를 사용한다.
사용한 라이브러리는 Moya로 Api 호출을 하고, Kingfisher로 이미지 로드와 캐싱을 처리했다.
실습에는 Kakao Search Image API를 사용했다.
https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-image
결과화면은 다음과 같다.
UI는 최소한으로 구성하여 그다지 신경쓰지 않았다.. 껄껄
요구사항을 하나하나 살펴보자
1. 텍스트 필트에 텍스트를 한 글자 입력할 때 마다 결과가 로드된다.
2. 입력 텍스트가 변경되면 데이터를 다시 받아온다.
homeView.searchBar.textfield.rx.text.orEmpty
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] text in
guard let self else { return }
viewModel.clearPageInfo()
requestApiFirstStep(text: text)
})
.disposed(by: disposeBag)
텍스트가 하나씩 입력될 때마다 api를 호출하면 된다.
2. UI는 image가 한줄에 3개씩, API 1회 호출 시 데이터가 20개씩 추가된다.
extension HomeVC: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.searchDatas.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = homeView.collectionView.dequeueReusableCell(withReuseIdentifier: HomeCVCell.identifier, for: indexPath) as? HomeCVCell else { return UICollectionViewCell() }
let urlString = viewModel.searchDatas[indexPath.row].imageURL
DispatchQueue.main.async {
cell.imageView.setImage(urlString: urlString)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numberOfItemsPerRow: CGFloat = 3
let spacingBetweenItems: CGFloat = 10
let totalSpacing = (numberOfItemsPerRow - 1) * spacingBetweenItems
let availableWidth = homeView.collectionView.frame.width - totalSpacing
let calculatedItemWidth = availableWidth / numberOfItemsPerRow
return CGSize(width: calculatedItemWidth,
height: calculatedItemWidth)
}
}
3개씩 한줄로 만드는 코드는 sizeForItemAt에 작성된 부분이다.
컬렉션 뷰의 너비에서 한 줄에 들어가는 셀 간의 간격의 크기를 뺀다. 이는 numberOfItemsPerRow - 1 의 값이 되며 이 값에 셀 간에 간격에 해당하는 spacingBetweenItems의 값을 곱해서 뺀 후 전체 컬렉션뷰 너비에서 내가 한줄에 배치할 아이템의 개수 만큼 나누면 된다.
4. 가장 아래로 스크롤 하면 다음 결과를 불러온다.
extension HomeVC : UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let height = scrollView.frame.height
let isEndPosition = offsetY > (contentHeight - height)
if isEndPosition && viewModel.isEnabledPaging {
let text = homeView.searchBar.textfield.text ?? ""
requestApiMoreStep(text: text)
}
}
}
먼저 테이블 뷰, 컬렉션 뷰에는 기본으로 스크롤뷰가 내장되어 있음을 알아야한다.
컬렉션 뷰의 가장 아래로 스크롤하는 상황을 그림으로 그리면 아래와 같다.
그림처럼 휴대폰에서 컬렉션 뷰가 차지하는 높이는 717이라는 값이고, 내가 api를 호출하여 데이터를 불러올 때 마다 추가로 들어오는 데이터에 대한 컨텐츠 높이가 876인 상황이다.
그림 점선으로 표시한 것과 같이 화면에는 보이지 않지만 api 호출 할 때마다 876만큼의 컨텐츠 높이가 추가된다.
내가 스크롤 한 offsetY는 198.xx, 데이터가 그려진 contents의 높이는 876.xx, 그리고 화면의 높이(화면에 보이는 컬렉션뷰의 높이)는 717이다.
offsetY는 내가 스크롤 할 때 마다 변경될 것이고, contentHeight는 api가 호출 될 때마다 876만큼 더 추가될 것이고, height는 변하지 않을 것이다.
화면을 처음 키면 내 화면에는 876만큼의 컨텐츠 내용 중 717의 높이만큼 보일 것이고, 이때 offsetY는 0이다. 따라서 보이지 않는 876 - 717 = 159 만큼의 영역을 내가 스크롤 할 수 있는 영역이 되고, 그러므로 offsetY가 159보다 커지게 되면 컬렉션 뷰 가장 아래에 도달했음을 알 수 있다.
가장 아래에 도달 한 후 값을 보면 내 예상과 다르게 api 3회 호출 만큼의 컨텐츠(876 * 3 = 2628)가 추가되어 있다.
이를 방지하기 위해 가장 아래에 도달하여 api를 호출하자마자 isEnabledPaging라는 프로퍼티를 만들어 isEnabledPaging = false로 설정하여 이중 호출을 방지한다.
api 호출 완료 후 isEnabledPaging = true로 하면 된다.
5. 이미지는 캐싱처리되어 이후 필요할 경우 캐싱된 데이터를 사용한다.
extension UIImageView {
func setImage(urlString: String) {
ImageCache.default.retrieveImage(forKey: urlString) { result in
switch result {
case .success(let value):
// 캐시 존재
if let image = value.image {
self.image = image
} else {
//캐시 미 존재
guard let url = URL(string: urlString) else { return }
self.kf.setImage(with: url)
}
case .failure(let error):
print(error)
}
}
}
}
요 단계에서 Kingfisher를 사용한다.
urlString을 전달하여 이미지를 설정하면 Kingfisher가 알아서 이미지 캐시가 있으면 캐시데이터를 던져주고 아니면 새로 생성한다. 굳굳
Kakao Image Search API를 사용하면서 몇몇 이미지 url이 http여서 화면에 나타나지 않는 이슈가 있었다.
Target - Info에서 App Transport Security Settings -> Allow Arbitrary Loads = True로 하면 http 이미지도 로드가 가능하다!
전체 코드는 요기!
https://github.com/hililyy/ios-practice/tree/main/Pagination
'iOS' 카테고리의 다른 글
[iOS/RxSwift] Merge, CombineLatest, Zip 비교하기 (0) | 2024.01.20 |
---|---|
[iOS] NIB View 생성자로 인스턴스 생성하기 (BaseView, 클린코드 독후감..) (1) | 2023.11.27 |
[iOS] Keychain(키체인) 알아보기 (0) | 2023.07.31 |
[iOS] UIImageView Orientation과 이미지를 회전하는 방법 (0) | 2023.07.29 |
[iOS] iOS 푸시알림(APNS, FCM) (0) | 2023.06.29 |