티스토리 뷰

반응형

이 글의 목적은 header, footer가 있는 collectionview를 Paging 또는 Carousel 또는 Snap 하기--!! 인데요,

점진적으로(??) 설명하겠습니다.

 

[1] cell 간 spacing이 없는 CollectionView (화면 사이즈와  cell 사이즈가 같은 collectionView)

cell size가 화면 size와 같다면 

collectionView.pagingEnabled = true로 하면 이렇게 center로 스크롤이 잘 멈춥니다.

 

 

 

 

 

[2] cell 간 spacing이 있는 collectionView

 

하지만 이런식으로 셀들간의 spacing을 주게 된다면

let collectionViewLayout: UICollectionViewFlowLayout = {
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: self.view.bounds.width - 40, height: self.view.bounds.height - 80)
    layout.minimumLineSpacing = 20
    layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
    layout.scrollDirection = .horizontal
    return layout
}()

 

collectionView.pagingEnabled = true는 우리가 바라는 대로 동작하지 않습니다.

 

 

 

 

그래서 center paging을 하고 싶다면

멈추는 포인트를 센터로 직접바꿔줘야합니다.

 

2가지 방법이 있습니다. 

두가지 방법 모두, 우선 설정을 이렇게 해줘야합니다.

  collectionView.decelerationRate = .fast
  collectionView.isPagingEnabled = false

 

방법 1) scrollViewWillEndDragging 에서 하기

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        guard let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        
        let cellWidthIncludingSpacing = layout.itemSize.width + layout.minimumLineSpacing
        
        let estimatedIndex = scrollView.contentOffset.x / cellWidthIncludingSpacing
        let index: Int
        if velocity.x > 0 {
            index = Int(ceil(estimatedIndex))
        } else if velocity.x < 0 {
            index = Int(floor(estimatedIndex))
        } else {
            index = Int(round(estimatedIndex))
        }
        
        targetContentOffset.pointee = CGPoint(x: CGFloat(index) * cellWidthIncludingSpacing, y: 0)
    }

 

참고:

gist.github.com/gwangyonglee/74133ab58261dbe1df460e34215f4aae

www.youtube.com/watch?v=XKXFRHctC6o

 

 

방법 2) custom layout 만들기 

class CustomCollectionViewFlowLayou: UICollectionViewFlowLayout {

    private var previousOffset: CGFloat = 0
    private var currentPage: Int = 0

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        }

        let itemsCount = collectionView.numberOfItems(inSection: 0)

        // Imitating paging behaviour
        // Check previous offset and scroll direction
        if previousOffset > collectionView.contentOffset.x && velocity.x < 0 {
            currentPage = max(currentPage - 1, 0)
        } else if previousOffset < collectionView.contentOffset.x && velocity.x > 0 {
            currentPage = min(currentPage + 1, itemsCount - 1)
        }

        // Update offset by using item size + spacing
        let updatedOffset = (itemSize.width + minimumInteritemSpacing) * CGFloat(currentPage)
        previousOffset = updatedOffset

        return CGPoint(x: updatedOffset, y: proposedContentOffset.y)
    }
}

 

customlayout을 collectionView의 layout에 넣어주세요-!

  let collectionViewLayout: UICollectionViewFlowLayout = {
      let layout = CustomCollectionViewFlowLayout()
      layout.itemSize = CGSize(width: self.view.bounds.width - 40, height: self.view.bounds.height - 80)
      layout.minimumLineSpacing = 20
      layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
      layout.scrollDirection = .horizontal
      return layout
  }()

  collectionView.collectionViewLayout = collectionViewLayout

 

참고:

 stackoverflow.com/questions/23990863/uicollectionview-cell-scroll-to-centre

 

그러면 이렇게 센터로 스크롤이 잘 멈춥니다. 

 

 

 

 

 

[3] header, footer가 있는 collectionView

 

collectionView에 헤더와 푸터를 넣어주면 어떻게 될까요?!

위의 두 방법을 썼을때 센터에 위치 안합니다. 당연히 셀들만 고려해서 센터 포인트를 잡는 방식이기 때문이에요 

 

 

 

 

 

해결방안 1)

 

그럴땐 이런 레이아웃을 쓰면 됩니다.

현재 스크롤된 위치에서 collectionView.bounds(화면 사이즈)를 rect으로 잡아서

그 rect 안에 있는 elements들 (헤더, 푸터, 셀) 중, 스크롤 해서 가려고 하는 후보를 찾습니다.

그리고 그 후보의 센터로 이동하는 코드입니다.

그래서 헤더, 푸터가 있어도 center paging이 동작합니다. 

(아래 코드의 출처는 까먹었습니다ㅠㅠ죄송해요ㅠㅠ)

class CenterCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else { return .zero }

        let halfWidth = collectionView.bounds.size.width / 2
        let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth

        guard let attributesOfVisibleElements = self.layoutAttributesForElements(in: collectionView.bounds) else { return .zero }

        var candidate: UICollectionViewLayoutAttributes?
        for attributes in attributesOfVisibleElements {
            if candidate == nil {
                candidate = attributes
                continue
            }
            
            let a = attributes.center.x - proposedContentOffsetCenterX
            let b = candidate!.center.x - proposedContentOffsetCenterX
            if abs(a) < abs(b) {
                candidate = attributes
            }
        }
        
        guard let currentAttributes = candidate else { return .zero }
        return CGPoint(x: currentAttributes.center.x - halfWidth, y: proposedContentOffset.y)
    }
}

 

다만 위의 로직은 header, footer 넓이가 이렇게 넓으면 괜찮은데,

    let collectionViewLayout: UICollectionViewFlowLayout = {
        let layout = CenterCollectionViewFlowLayout()
        layout.itemSize = CGSize(width: self.view.bounds.width - 40, height: self.view.bounds.height - 80)
        layout.minimumLineSpacing = 20
        layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
        layout.scrollDirection = .horizontal
        layout.headerReferenceSize = CGSize(width: 200, height: self.view.bounds.height - 80)
        layout.footerReferenceSize = CGSize(width: 300, height: self.view.bounds.height - 80)
        return layout
    }()

 

 

 

 

 

 

header, footer 넓이가 좀 작으면 첫번째 또는 마지막 셀로 가게 해준다는 오류(?)가 있어요.

 

    let collectionViewLayout: UICollectionViewFlowLayout = {
        ...
        layout.headerReferenceSize = CGSize(width: 100, height: self.view.bounds.height - 80)
        layout.footerReferenceSize = CGSize(width: 50, height: self.view.bounds.height - 80)
        return layout
    }()

 

첫번째 cell에서 swipe to left해서 header로 가고 싶은데 안가지는 움짤입니다. 

 

 

이 방법은 header, footer가 넓을때만 사용하세요-!! 

 

 

해결방안 2) 

 

"[2] cell 간 spacing이 있는 collectionView" 에서 소개한 방법이랑 비슷한 컨셉인데

조금 변형해서 헤더, 푸터를 고려해 준 방법이에요

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {

  private var previousOffset: CGFloat = 0
  private var currentPage: Int = 0

  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
      guard let collectionView = collectionView else {
          return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
      }

      if previousOffset > collectionView.contentOffset.x && velocity.x < 0 {
          currentPage = currentPage - 1
      } else if previousOffset < collectionView.contentOffset.x && velocity.x > 0 {
          currentPage = currentPage + 1
      }

      let additional = (itemSize.width + minimumLineSpacing) - headerReferenceSize.width
      let updatedOffset = (itemSize.width + minimumLineSpacing) * CGFloat(currentPage) - additional

      previousOffset = updatedOffset

      return CGPoint(x: updatedOffset, y: proposedContentOffset.y)
  }

 

이 방법을 쓰면 header 넓이가 작아도 첫번째 셀에서 swipe to left했을때 header로 잘 이동하더라구요-!!

 

 

 

만약에 custom collectionview layout 만들어서 넣어주기 싫다면 이렇게 할 수 도 있습니다

위의 것이랑 똑같은 방법이에요~

private var previousOffset: CGFloat = 0
private var currentPage: Int = 0

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let point = self.targetContentOffset(scrollView, withVelocity: velocity)
    targetContentOffset.pointee = point
}

func targetContentOffset(_ scrollView: UIScrollView, withVelocity velocity: CGPoint) -> CGPoint {

    guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return .zero }

            if previousOffset > collectionView.contentOffset.x && velocity.x < 0 {
                currentPage = currentPage - 1
            } else if previousOffset < collectionView.contentOffset.x && velocity.x > 0 {
                currentPage = currentPage + 1
            }

    let additional = (flowLayout.itemSize.width + flowLayout.minimumLineSpacing) - flowLayout.headerReferenceSize.width

    let updatedOffset = (flowLayout.itemSize.width + flowLayout.minimumLineSpacing) * CGFloat(currentPage) - additional

    previousOffset = updatedOffset

    return CGPoint(x: updatedOffset, y: 0)
}

 

 

하지만 문제가 있는데요... 위의 두 방법 모두 스크롤하다가 살짝 놓으면 파닥파닥 되는 현상이 조금 보입니다 😭

 

1번이 더 심합니다...!! 

 

1번 방법 움짤

 

 

 

위의 현상에 대한 원인과 해결방안을 며칠째 찾아봤는데 못찾었어요...!!! 

근데 완벽하지 않은 해결방안을 찾았는데요,

targetContentOffset.pointee 설정해주고

collectionView.setContentOffset(point, animated: true)  해주니까 안 파닥거리더라구요...!! (사실 이 코드를 해줄 필요가 없는 데 말이죠...!!) 

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let point = self.targetContentOffset(scrollView, withVelocity: velocity)
        targetContentOffset.pointee = point
        collectionView.setContentOffset(point, animated: true)
    }

 

 

 

그리고 애니매이션을 같이 넣어주면 더 부드럽게 스크롤 됩니다..! 

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let point = self.targetContentOffset(scrollView, withVelocity: velocity)
        targetContentOffset.pointee = point
        
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: {
            self.collectionView.setContentOffset(point, animated: true)
        }, completion: nil)
    }

 

 

 

 

⭐️ 최종 코드 ⭐️

 

 

참고로 header,footer 쓰겠다는 설정은 스토리 보드에서 해줬어요 :-)

 

 

 

 

 

⚠️ 오지랖 넓은 당부의 말씀 ⚠️

 

현재 있는 앱들 중 collectionview를 페이징하는데 header, footer가 있는 케이스를 못봤는데요

(앱스토어, 멜론 등 paging 되는 collectionView 들 모두 헤더, 푸터 없음)

 

그 이유가 있는 것 같습니다....

 

paging할껀데 헤더, 푸터 쓰고 싶다면 신중하게 고려하세요..😓.😓

 

 

 

 

더 좋은 방법을 찾으면 글에 첨부하겠습니다--!!

 

 

 

 

Repository 

https://github.com/eunjin3786/PagingCollectionViewExample

 

eunjin3786/PagingCollectionViewExample

Contribute to eunjin3786/PagingCollectionViewExample development by creating an account on GitHub.

github.com

 

반응형
댓글