swift

Infinite Carousel(무한 캐러셀)

kimyounggyun 2022. 9. 15. 23:44

다섯 개의 사진이 순서대로 돌아가는 캐러셀을 만들어보자.

 

컬렉션 뷰 사용하기

컬렉션 뷰의 페이징 기능을 사용해 캐러셀을 만들 수 있다. 수평 스크롤인 컬렉션 뷰를 추가하고 레이아웃을 잡은 후 컬렉션 뷰의 Paging Enabled 속성을 True로 바꾼다. 그리고 셀에 이미지 뷰를 넣으면 된다.

자연스러운 페이징을 위해 컬렉션 뷰의 Min Spacing을 0으로 한다. Min Spacing을 0으로 하지 않으면 이미지가 넘쳐서 보인다.

이제 셀 사이즈를 조절하여 한 개의 이미지씩 보이게 바꾸자. 

 

UICollectionViewDelegateFlowLayout 프로토콜을 사용해서 셀 아이템의 크기를 정하자.

extension RxViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView,
                      layout collectionViewLayout: UICollectionViewLayout,
                      sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: self.collectionView.frame.width,
                  height: self.collectionView.frame.height)
  }
}

무한 스크롤 구현하기

무한 스크롤 캐러셀을 만들기 위해서는 트릭이 필요하다. 실제로 보여주는 이미지는 5개이지만 첫 번째와 마지막 순서에 각각 마지막 이미지와 첫 번째 이미지를 추가해 총 7개의 이미지를 컬렉션 뷰가 보여주게 하는 것이다.

 

사용자 입장의 첫 번째 이미지(1번)에서 좌로 스크롤을 했을 때 실제로 컬렉션 뷰의 IndexPathrow 값은 0이지만 첫 번째 셀의 이미지와 사용자 입장의 마지막 이미지(5번)가 똑같기 때문에 마지막 페이지로 이동한 것과 같은 효과를 보여준다. 

마찬가지로 마지막 이미지(5번)에서 우로 스크롤했을 때 처음 이미지(1번)로 가는 것처럼 보이기 위해 마지막 셀(6번)의 이미지는 1번 셀의 이미지와 동일한다.

 

이제 0번 셀로 이동했을 때 컬렉션 뷰의 scrollToItem 메서드를 사용해 5번 셀로 이동시키면 마지막 이미지로 이동한 것과 같은 효과를 낼 수 있다. 마찬가지로 6번 셀로 이동했을 때 scrollToItem 메서드를 사용해 1번 셀로 이동시키면 첫 번째 이미지로 이동한 것과 같은 효과를 낼 수 있다.

 

먼저 viewDidLoad에서 컬렉션 뷰의 초기 셀을 1번 셀로 지정한다.

override func viewDidLoad() {
  super.viewDidLoad()
  bind()
  DispatchQueue.main.async {
    self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0),
                                     at: .right,
                                     animated: false)
  }
}

crollViewDidEndDecelerating(_:) 메서드를 사용해서 만약 첫 번째 셀(0번)로 이동했으면 scrollToItem 메서드를 사용해 마지막 이미지가 있는 셀(5번)로 이동하고, 마지막 셀(6번)로 이동했으면 첫 번째 이미지가 있는 셀(1번)로 이동한다.

collectionView.rx.didEndDecelerating
      .withUnretained(self)
      .observe(on: MainScheduler.asyncInstance)
      .subscribe(onNext: { vc, _ in
        let page =  Int(vc.collectionView.contentOffset.x / vc.collectionView.frame.width)
        if(page == 0) {
          vc.collectionView.scrollToItem(at: IndexPath(row: vc.imageArray.count - 2,
                                                       section: 0),
                                         at: .right,
                                         animated: false)
        }
        else if(page == vc.imageArray.count - 1) {
          vc.collectionView.scrollToItem(at: IndexPath(row: 1, section: 0),
                                         at: .right,
                                         animated: false)
          
        }
      })
      .disposed(by: disposeBag)

만약 페이지 컨트롤을 추가했을 경우 페이지 컨트롤의 페이지 개수는 이미지 배열의 길이 - 2와 같다.

self.viewModel.outputs.imageList
      .map { $0.count - 2 }
      .drive(pageControl.rx.numberOfPages)
      .disposed(by: disposeBag)

페이지 컨트롤의 현재 페이지는 컬렉션 뷰의 contentOffset를 사용하면 구할 수 있다. 

collectionView.rx.contentOffset
      .map { $0.x + (self.collectionView.frame.width / 2.0) }
      .map { Int($0 / self.collectionView.frame.width) - 1 }
      .bind(to: self.pageControl.rx.currentPage)
      .disposed(by: self.disposeBag)

하지만 위와 같이 바인딩했을 경우 오른쪽과 다르게 왼쪽처럼 첫 번째 마지막 페이지 간의 페이지 컨트롤 이동이 부자연스럽다.

 

그 이유는 .map { Int($0 / self.collectionView.frame.width) - 1 }의 값이 -1과 5가 나올 수 있어 페이지 컨트롤의 페이지 개수를 넘어가는 인덱스가 나오기 때문이다. 따라서 -1과 5일 때 페이지 컨트롤의 값을 바꿔주면 된다.

 

collectionView.rx.contentOffset
      .map { $0.x + (self.collectionView.frame.width / 2.0) }
      .map { Int($0 / self.collectionView.frame.width) - 1 }
      .map({ page in
        if page == -1 {
          return self.imageArray.count - 2
        }
        else if page == self.imageArray.count - 2 {
          return 0
        }
        return page
      })
      .bind(to: self.pageControl.rx.currentPage)
      .disposed(by: self.disposeBag)