사실 나는 처음에 마지막 한 칸이 저렇게 비어있는 게 거슬려서 '아니 포켓몬을 21마리씩 불러오라고 하지 왜 20마리씩 불러오래😒' 라고 생각하며 내 맘대로 21마리로 불러왔었다. (처음 포켓몬도 이상해씨 아니고 피카츄가 나오게 했었다.) 그런데 레벨 8을 구현하면서 괜히 그런 게 아니었다는 걸 느꼈다.
사용자 입장에서는 아이템이 추가된 걸 알아채기 힘든 모습
그래서 짧은 반성의 시간을 가진 뒤 다시 순순히 20개씩 부르는 걸로 수정했다🤓
UICollectionView
가 UIScrollView
를 상속하는 것처럼, UICollectionViewDelegate
도 UIScrollViewDelegate
를 상속한다. 그래서 didSelectItemAt
을 구현하기 위해 컬렉션 뷰 델리게이트를 채택했다면 그곳에서 스크롤 뷰 델리게이트 메소드들도 호출할 수 있다.
UICollectionViewDelegate를 채택했을 때
collectionView 메소드와 scrollView 메소드가 모두 호출 가능한 모습
우선 한 번에 포켓몬 20마리씩 호출이 되고, 그 때 끝까지 스크롤을 내리면
새로운 20마리를 호출하는 것이기 때문에, 끝까지 스크롤을 내린 시점을 판별하는 게 중요하다. 예전 프로젝트에서 페이지컨트롤을 구현한 뒤 가로 스크롤도 같이 공부했던 내용이 떠올랐다. 이때 스크롤이 얼마나 되었는지 확인할 수 있는 값에는 contentOffset
이 있다는 것을 알았다.
진한 파랑
이 화면에 보이는 visible
영역, 옅은 파랑
이 실제 content
전체의 영역이라고 했을 때, scrollView
의 top
이 y축의 0
, leading
이 x축의 0
지점이다.
vertical scroll
의 경우 content
가 위로
올라갈수록 contentOffset
이 늘어나고, horizontal scroll
의 경우 왼쪽(leading)
으로 밀릴수록 contentOffset
이 늘어난다.
즉,
contentOffset
값은content의 top/leading
과visible의 top/leading
간의 거리다.
이번 과제의 컬렉션 뷰는 vertical scroll view
기 때문에, 스크롤 뷰 델리게이트의 메소드 중 스크롤이 될 때마다 호출되는 scrollViewDidScroll
을 통해서 contentOffset
의 y
값을 확인해보겠다.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset.y)
}
끝까지 내렸을 때 contentOffset.y의 값은 314.0이다.
이 값을 활용해 스크롤이 끝까지 내려간 순간을 인식하고 fetch
함수를 호출하면 되겠다는 생각으로 이어졌다.
UIScrollViewDelegate
에는 여러가지 메소드가 있지만, 그 중에서 scrollViewDidEndDecelerating
는 스크롤이 움직임을 멈췄을 때
호출된다. 그렇다면 움직임이 멈추고, 그때 contentOffset.y
값이 314.0
이라면 호출하도록 해야겠다!
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
print("contentOffset.y = \(scrollView.contentOffset.y)")
if scrollView.contentOffset.y == 314.0 {
print("go fetch!")
}
}
하지만 이건 일차원적인 생각이었다. offset
은 계속 늘어날텐데😒 그렇다면 한번 올라갈 때마다 (scrollView 높이 + contentOffset.y) * n
씩 늘려줘야 하나? 이런 생각으로 처음에는 전역변수에 314
를 두고 스크롤을 내릴 때마다 값을 더해주는 방식을 생각했다가, 한번에 20칸
씩 늘어나기 때문에 스크롤 밖 영역의 값이 일정하게 늘어나지 않겠다는 생각이 들어 고민에 빠졌다. (첫번째는 한칸이 비지만 두번째는 두칸이 비고, 세번째는 꽉찬다. 즉 3번 주기로 화면 모양이 바뀌는 건데 2번째 화면이 1,3번째 화면보다 좀 길다.)
그렇게 고민에 빠진 채 디버깅으로 contentSize
값이랑 contentOffset
값 출력하면서 숫자 더하기 빼기 해보다가 contentSize.height - contentOffset.y
값이 일정한 걸 발견했고 (알고 생각해낸 게 아니라 정말 우연히 발견했다.) 이걸 통해 무한스크롤 구현에 성공했다. 저 일정한 값이 visibleSize.height
라는 건 구현한 다음에 깨달았다.
처음에 성공하고 진짜 이 상태였음
하여튼 무한 스크롤에 성공한 코드는 이렇다.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentSize.height - scrollView.contentOffset.y == scrollView.visibleSize.height {
vm.fetchPokemonList()
}
}
// scrollView.visibleSize.height 값이 624인데, 처음에는 어 두개 빼니까 계속 624네? 하고 624 넣었었다.
이게 무슨 말이냐하면 contentSize
는 컬렉션 뷰 전체의 크기를 말하는데, 그 높이 값(scrollView.contentSize.height
)에서 스크롤 위로 벗어난 영역의 높이(scrollView.contentOffset.y
)를 빼줬을 때, 화면에 눈으로 보이는 영역의 높이(scrollView.visibleSize.height
)와 같다면 그게 바로 스크롤이 제일 밑으로 내려간 상태일 테니, 그때 fetch
메소드를 호출하겠다는 것이다.
MainViewModel
에서는 fetch
메소드가 호출될 때마다 포켓몬 id
값을 20씩 늘린 뒤 URL 파라미터
에 전달하도록 처리했다.
private var offset: Int = -20
func fetchPokemonList() {
offset += 20
provider.request(.fetchURL(offset: offset)) { [weak self] result in
switch result {
case .success(let response):
do {
let data = try JSONDecoder().decode(PokemonURL.self, from: response.data)
self?.pokemonList.onNext(data.results)
} catch {
self?.pokemonList.onError(NetworkError.decodingFailed)
}
case .failure(let error):
self?.pokemonList.onError(error)
}
}
}
알고 생각해낸 게 아니라서 조금 쑥스럽긴 하지만 그래도 contentOffset
을 어떻게든 이용해보겠다고 깔짝거린 게 보람이 없지는 않은 거 같다. 결국 구현한 뒤라도 이해하긴 했으니깐😄 만족스럽다.
무한 스크롤 완성
피카츄 편애를 멈춰주세요