티스토리 뷰
[UICollectionView] Paging / Carousel / Snap Collectionview with header, footer
eungding 2020. 7. 10. 21:05이 글의 목적은 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번이 더 심합니다...!!
위의 현상에 대한 원인과 해결방안을 며칠째 찾아봤는데 못찾었어요...!!!
근데 완벽하지 않은 해결방안을 찾았는데요,
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
'🍏 > iOS' 카테고리의 다른 글
- Total
- Today
- Yesterday
- ipad multitasking
- DRF APIException
- flutter 앱 출시
- ribs
- flutter dynamic link
- drf custom error
- Python Type Hint
- PencilKit
- Flutter Clipboard
- Django Heroku Scheduler
- flutter deep link
- Django FCM
- 플러터 싱글톤
- Flutter Spacer
- 플러터 얼럿
- Django Firebase Cloud Messaging
- cocoapod
- METAL
- Dart Factory
- Flutter 로딩
- 장고 Custom Management Command
- Sketch 누끼
- 구글 Geocoding API
- SerializerMethodField
- Flutter getter setter
- flutter build mode
- 장고 URL querystring
- Watch App for iOS App vs Watch App
- github actions
- Flutter Text Gradient
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |