728x90
UICollectionView에서 cell 수직방향 정렬
UICollectionView 사용 시, 기본적으로 같은 line에 있는 cell들은 모두 중앙 정렬이 된다.
하지만 CollectionView를 사용하다보면 중앙 배치보단 상하, 혹은 좌우 한 방향으로 정렬시키는 경우가 훨씬 많다.
그래서 Custom CollectionViewFlowLayout을 만들어서 수직 스크롤 시 한 방향으로 정렬시킬 수 있도록 해보았다.
https://github.com/Sky-Titan/TSCollectionViewVerticalAlignLayout
정렬 enumeration
enum CollectionViewVerticalAlign {
case top
case center
case bottom
}
수직 정렬만 지원할 것이기에 top, center, bottom case만 만들었다.
CollectionViewLayoutAttributeSector
class CollectionViewLayoutAttributeSector {
var attributes: [UICollectionViewLayoutAttributes] = []
let centerY: CGFloat
var minX: CGFloat {
return attributes.reduce(CGFloat.infinity, { result, attribute in
min(result, attribute.frame.minX)
})
}
var maxX: CGFloat {
return attributes.reduce(CGFloat.zero, { result, attribute in
max(result, attribute.frame.maxX)
})
}
var minY: CGFloat {
return attributes.reduce(CGFloat.infinity, { result, attribute in
min(result, attribute.frame.minY)
})
}
var maxY: CGFloat {
return attributes.reduce(CGFloat.zero, { result, attribute in
max(result, attribute.frame.maxY)
})
}
var rect: CGRect {
return CGRect(x: 0, y: minY, width: UIScreen.main.bounds.width, height: maxY - minY)
}
init(centerY: CGFloat) {
self.centerY = centerY
}
}
같은 줄에 있는 cell들을 포함시키는 Sector 클래스이다.
수직 정렬 시엔, centerY가 같으면 같은 줄에 있다는 의미이므로 centerY 값을 key로 하여 같은 centerY값을 가지는 attributes들을 가진다.
그리고 후에 특정 영역 안에 노출되어야할 cell들을 반환하는 로직을 만들 때, 같은 sector에 있으면 무조건 함께 노출시켜야 하기에, Sector의 rect를 따로 만들었다.
TSCollectionViewVerticalAlignLayout
class TSCollectionViewVerticalAlignLayout: UICollectionViewFlowLayout {
private var sectors: [CGFloat: CollectionViewLayoutAttributeSector] = [:]
required init(verticalAlign: CollectionViewVerticalAlign) {
self.verticalAlign = verticalAlign
super.init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
var verticalAlign: CollectionViewVerticalAlign = .center {
didSet {
invalidateLayout()
}
}
override func prepare() {
super.prepare()
refreshSectors()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard scrollDirection == .vertical else {
return super.layoutAttributesForElements(in: rect)
}
var attributes: [UICollectionViewLayoutAttributes]?
sectors.enumerated().forEach({
let sector = $0.element.value
if sector.rect.intersects(rect) {
if attributes == nil {
attributes = []
}
attributes?.append(contentsOf: sector.attributes.compactMap({
if let kind = $0.representedElementKind {
return layoutAttributesForSupplementaryView(ofKind: kind, at: $0.indexPath)
}
return layoutAttributesForItem(at: $0.indexPath)
}))
}
})
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attribute = super.layoutAttributesForItem(at: indexPath)
guard scrollDirection == .vertical else {
return attribute
}
guard let attribute = attribute else {
return nil
}
let centerY = attribute.center.y
if let sector = sectors[centerY] {
switch verticalAlign {
case .top:
attribute.frame.origin.y = sector.minY
case .bottom:
attribute.frame.origin.y += sector.maxY - attribute.frame.maxY
default:
break
}
}
return attribute
}
private func refreshSectors() {
sectors = [:]
let allLayoutAttributes = allLayoutAttributes()
allLayoutAttributes.forEach({
let centerY = $0.center.y
if sectors[centerY] == nil {
sectors[centerY] = CollectionViewLayoutAttributeSector(centerY: centerY)
}
sectors[centerY]?.attributes.append($0)
})
}
private func allLayoutAttributes() -> [UICollectionViewLayoutAttributes] {
var list: [UICollectionViewLayoutAttributes] = []
for section in 0 ..< numberOfSection() {
if let headerAttribute = super.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(row: 0, section: section)) {
list.append(headerAttribute)
}
for row in 0 ..< numberOfItems(section) {
let indexPath: IndexPath = IndexPath(row: row, section: section)
if let attribute = super.layoutAttributesForItem(at: indexPath) {
list.append(attribute)
}
}
if let footerAttribute = super.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: IndexPath(row: numberOfItems(section), section: section)) {
list.append(footerAttribute)
}
}
return list
}
func numberOfSection() -> Int {
if let collectionView = collectionView, let number = collectionView.dataSource?.numberOfSections?(in: collectionView) {
return number
}
return 0
}
func numberOfItems(_ section: Int) -> Int {
if let collectionView = collectionView, let number = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section) {
return number
}
return 0
}
}
- sectors:
- 모든 cell들은 sector단위로 구분해서 가지고 있는 캐시 역할을 하는 dictionary이다.
- centerY값을 key로 해서 sector들을 들고 있다.
- verticalAlign:
- 정렬 값을 정해주는 프로퍼티
- 설정될 때마다 layout을 invalidate 시키고 새로 그려준다.
- refreshSectors()
- sector들을 다시 만들어주는 역할을 한다.
- layout이 invalidate 될 때마다 호출되어야 하므로, prepare함수 안에서 매번 호출한다.
- allLayoutAttributes()
- 모든 layoutAttributes들을 불러오는 역할을 한다.
- sector분할은 기존 super class의 값들을 기준으로 하므로 super class의 layout attributes들을 불러온다.
- layoutAttributesForElements(in:)
- rect안에 포함되어야할 header, footer, cell들의 attributes들을 모두 포함한 array를 반환한다.
- scroll direction이 horizontal인 경우엔 super class의 attributes들을 그대로 반환한다.
- sectors 딕셔너리에 있는 sector들을 모두 검사하고, header, footer인 경우엔 가공없이 그대로 포함시키고 일반 cell들은 정렬 값에 맞게 frame을 가공시킨다.
- layoutAttributesForItem(at:)
- indexPath에 해당하는 attributes 객체를 반환한다.
- scroll direction이 horizontal인 경우엔 super class의 attribute를 그대로 반환한다.
- 해당 함수를 호출할 때, attribute에 속해있는 sector의 minY, maxY값을 보고, originY 값을 새로 계산해 지정해준다.
- top 정렬: sector의 minY값으로 originY값을 지정해준다.
- bottom 정렬: sector의 maxY값과 attribute의 기존 maxY값의 차이만큼 originY에 더해준다,.
- center 정렬: 아무런 가공없이 그대로 내보낸다.
Custom Layout 적용 전
Custom Layout 적용 후
이번엔 수직 정렬만 만들었지만 수평 정렬도 동일한 원리로 만들 수 있다.
728x90
'iOS > 예제' 카테고리의 다른 글
[iOS 예제] SlideToUnlockView (0) | 2022.08.21 |
---|---|
[iOS 예제] CircularCarouselBannerView (0) | 2022.08.19 |
[iOS 예제] Recording Wave View 만들기 (0) | 2022.07.02 |
[iOS 예제] Roulette 만들기 (0) | 2022.06.25 |
[iOS 예제] Ripple Effect (0) | 2022.06.19 |
댓글