Carousel CollectionView 만들어보기

sanghoon Ahn·2021년 6월 2일
4

Daily Issue

목록 보기
5/9

Daily Issue #5

안녕하세요 dvHuni입니다!

주변에서 벌써 백신을 맞았다는 이야기가 조금씩 들려오네요!!😆

저도 빨리 접종해서 해외여행 가고싶네요 ~ ✈️

그날까지 계속 포스팅 하면서~

데일리 이슈의 다섯 번째 포스트 !! 시작해보겠습니다~!🤠


무엇이 문제입니까?

오늘은 CollectionVIew로 Carousel을 만들어 볼 예정입니다 !!!

Carousel이란 회전목마 라는 의미로써

보통 이벤트 배너같은곳에 많이 사용되는데요,

아래와 같이 항목을 슬라이드 해서 확인 할 수 있는 형태의 Layout 입니다!

이미지 출처(https://github.com/nicklockwood/iCarousel)

하지만 여기서 가장 중요한 점이 있습니다.

보통 회전목마 라고 하면 돌다보면 제자리가 되잖아요 ?? 끝이 없이 계속 도는거죠,

위의 예시는 끝이 정해져 있습니다. 😱

(Carousel보다는 Slider가 맞지 않을까)

그렇다면 어떻게 끝없이 도는 Carousel을 만들 수 있을까?

제가 생각해본 내용은 다음과 같습니다.

🌟 무한히 도는 Carousel 만들려면 어떻게 해야할까 🌟

  1. 보여줘야 하는 아이템 끝에 도착하면 보여줘야 하는 아이템들을 새로 덧붙여서 보여주면 되지않을까?
  2. 끝 항목의 다음 아이템은 첫번째 혹은 마지막 아이템으로 강제로 이동시키면 되지않을까?

이렇게 두가지 방법이 있습니다.

우선 1번 방법은 새로 덧붙이면 나중에는 항목이 겉잡을 수 없이 늘어나 메모리가 감당 할 수 없을지도 모릅니다.

그러므로 2번 방법으로 도전해 보려고 합니다. 고고🤜

먼저 레이아웃은 화면 중간에 CollectionView를 올려보겠습니다!!

참고로 모든 코드는 깃허브에 있습니다!! 레이아웃 잡는것은 코드를 확인해주세요 !🙇🏻‍♂️

CollectionView의 역할을 간단하게 설명 드리면,

dataSource에 있는 컬러값을 가져와서, Carousel item의 backgroundColor를 변경합니다.

private let dataSource: [UIColor] = [.orange, .brown, .blue, .gray, .cyan]

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SimpleCarouselCell.description(), for: indexPath) as? SimpleCarouselCell else { return UICollectionViewCell() }
    cell.backgroundColor = dataSource[indexPath.row]
    return cell
}

2번 방법대로 하기 위해서는 마지막 항목에서 다음 항목으로 넘어갈 때 를 알아야 합니다.

scrollView의 delegate method 중에서는 움직임을 감지하는 method가 여러가지 있는데요,

💡 참고로 CollectionViewDelegate는 UIScrollViewDelegate 상속받기 때문에 delegate method를 사용 할 수 있습니다!

사용할 것 같은 몇가지의 메소드를 잠깐 볼까요 ?

  1. scrollViewDidScroll

    → method이름 그대로 스크롤뷰(collectionView)가 스크롤 되는 동안 계속해서 호출됩니다.

  2. scrollViewWillBeginDragging

    → 스크롤뷰가 드래그가 시작 되려 할 때 호출됩니다.

  3. scrollViewDidEndDragging

    → 스크롤뷰가 드래그가 끝났을 때 호출됩니다.

  4. scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)

    → 스크롤뷰 드래그가 끝나려 할 때 호출 됩니다.

    • velocity(속도값)

      터치가 끝났을 때의 드래그 속도값입니다. x축 드래그면 x값, y축 드래그면 y값이 들어옵니다. 다음/이전 아이템으로 넘어가지 않는 속도값이라면 0을 return 합니다.

    • targetContentOffset

      contents가 정지해야 할 offset

그렇다면 우리는 마지막 항목에서 다음 항목으로 넘어갈 때를 알아야 하니까,

scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) method가 적당할 것 같습니다!

scrollView의 contentOffset으로 현재 콘텐츠의 offset을 이용하면 항목이 어디로 이동하려 하는지 파악 할 수 있을 것 같습니다.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let endOffset = scrollView.contentSize.width - carouselCollectionView.frame.width
    
    if scrollView.contentOffset.x < .zero && velocity.x < .zero {
        print("처음 -> 마지막")
    } else if scrollView.contentOffset.x > endOffset && velocity.x > .zero  {
        print("마지막 -> 처음")
    }
}

잘 캐치 되네요!! 이제 분기에서 원하는 항목으로 이동시켜보겠습니다!!

그전에 중요한점이 하나 있습니다!! 🙄

scrollViewWillEndDragging method안에서 항목을 이동시키면 Dragging Animation이 아직 끝난 상태가 아니기 때문에 다음 항목으로 이동하니 주의해야 합니다.

그렇다면 Dragging Animation이 끝난 상태는 어디일까요 ?

delegate method중 scrollViewDidEndDragging이겠죠 ?!

여기에 특정 항목으로 이동하는 코드를 넣고 동작을 보면 ...

carouselCollectionView.scrollToItem(at: IndexPath(row: dataSource.count - 1, section: .zero), at: .centeredHorizontally, animated: true)

원하는대로 움직이지 않습니다 ㅋㅋㅋ

정확한 이유는 모르겠습니다만, 추측해보면 Drag의 End가 Dragging Animation의 끝은 아닌것 같습니다.

그러면 그 뒤에 호출될 scrollViewWillBeginDecelerating method를 보겠습니다.

Dragging이 끝난 후 Decelerating이 시작 될 때 호출 되는 method 입니다.

여기에 작성하면 의도한대로 동작합니다!

animated를 true이기 때문에 scrollToItem이 scroll animation을 가지고 움직입니다.

false를 주게되면 animation없이 툭, 하고 아이템이 전환되버립니다.

이런건 우리가 원하는 방향이 아닐겁니다. 😵

그렇다면 여기에서 조금 방법을 틀어서 1번 방법에서 사용하려고 했던 아이템 덧붙이기를 살짝 응용해 볼까 합니다.

아이템을 무한히 붙인다면 메모리 문제가 발생할 테니, 몇 세트만 붙이는겁니다. 앞뒤로 한세트 정도 ???

그러면 [복제 세트 1][원래 세트][복제 세트2] 이런 데이터가 되겠죠?!

그리고 원래세트의 처음과 끝을 체크해서

  1. 원래 세트의 끝을 지나 복제세트 2의 첫번째로 왔다면, 원래세트의 첫번째로
  2. 원래 세트의 처음을 지나 복제세트 1의 마지막으로 왔다면, 원래세트의 마지막으로

위의 두가지 방법을 사용하면 animation도 살리고, carousel도 될것 같습니다. 출발해 보겠습니다!

/* 앞 뒤로 한세트식 덧붙인 dataSource */
private lazy var increasedDataSource: [UIColor] = {
   dataSource + dataSource + dataSource
}()

/* carousel 시작은 원래 세트여야 하기 때문에 레이아웃이 그려진 후, carousel의 시작점을 지정합니다. */
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    carouselCollectionView.scrollToItem(at: IndexPath(item: increasedDataSource.count / 3, section: 0),
                                        at: .centeredHorizontally,
                                        animated: false)
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let beginOffset = carouselCollectionView.frame.width * CGFloat(originalDataSourceCount)
    let endOffset = carouselCollectionView.frame.width * CGFloat(originalDataSourceCount * 2 - 1)
    
    if scrollView.contentOffset.x < beginOffset && velocity.x < .zero {
        scrollToEnd = true
    } else if scrollView.contentOffset.x > endOffset && velocity.x > .zero {
        scrollToBegin = true
    }
}

/* **scrollViewWillBeginDecelerating이 아닙니다!!** */
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    if scrollToBegin {
        carouselCollectionView.scrollToItem(at: IndexPath(item: originalDataSourceCount, section: .zero),
                                            at: .centeredHorizontally,
                                            animated: false)
        scrollToBegin.toggle()
        return
    }
    if scrollToEnd {
        carouselCollectionView.scrollToItem(at: IndexPath(item: originalDataSourceCount * 2 - 1, section: .zero),
                                            at: .centeredHorizontally,
                                            animated: false)
        scrollToEnd.toggle()
        return
    }
}

우선 viewDidLayoutSubviews를 먼저 봐주세요!

dataSource를 3배로 늘린것은 <왼쪽 / 오른쪽> 어디로 스크롤을 하더라도 연결되어있다는 느낌을 주기 위해서 입니다.

그렇다면 <왼쪽 / 오른쪽> 어디든지 스크롤 할 수 있기 때문에 시작점은 항상 원래 세트여야 합니다.

원래세트에서 시작하기 위해 layout이 초기화 된 viewDidLayoutSubviews에서 carousel의 시작점을 원래세트 아이템의 첫번째로 맞춰주어야 합니다!

한가지 더!

방금까지는 scrollViewWillBeginDecelerating에 작성하던 scrollToItem method를 왜 갑자기 scrollViewDidEndDecelerating에 작성했나요!

이유가 있습니다 ㅎㅎㅎㅎ😄

우선 기존에 작성했던 method는 scrollViewWillBeginDecelerating이기 때문에 후에 추가적으로 decelerating animation이 남아있습니다.

우리가 원했던 것은 animation은 완벽히 처리 되고, item은 원래 세트의 item으로 이동하는 것 입니다.

그래서 scrollViewDidEndDecelerating에서 decelerating animation이 끝난 후 scroll animation없이 원래 세트의 item으로 이동합니다.

결과를 같이 볼까요~?

하단 indicator를 보시면 확실히 원래 세트의 아이템으로 돌아가는 것을 확인 할 수 있습니다!!!

indicator가 없으면 확실히 계속 순환하는것 처럼 보이겠네요 ~~

여기에 Timer를 붙여서 일정 시간이 지나면 자동으로 스크롤을 되게 하거나

직접 indicator를 만들어서 응용할 수 있겠죠?!

이어붙인 데이터가 2세트 밖에 되지 않으므로, delegate method가 callback 되기 전에 계속해서 스크롤을 이어가면 아이템의 끝을 볼 수 있긴 합니다.

Carousel에는 문제가 없습니다만, 언제나 유저는 극한으로 몰고 가기 때문에 ...😵

이어붙일 데이터를 조금 더 여유롭게 두시는것을 추천드립니다!


Daily Issue라는 거창한 명목으로 초보 개발자가 뻘짓 하는것을 같이 봐주시고 계시는데요,

이번 포스트는 Carousel을 만들어보는 내용이였습니다!

Carousel은 이제 자신있게 구현할수 있을것 같습니다. ㅎㅎㅎㅎ

scrollViewDelegate method를 자세하게 살펴보았던 점도 좋았습니다.

어떤 시점에 호출되는지 알았고, 어느 타이밍에 동작을 넣어야 할지 감도 살짝 잡히네요 🙂

전체코드는 깃허브에 있습니다!

지적이나 질문은 저에게 큰 도움이 됩니다 🤓

오늘도 읽어주셔서 감사합니다 🙇🏻‍♂️

profile
hello, iOS

0개의 댓글