UICollectionViewLayout
콜렉션 뷰의 레이아웃 정의를 위한 추상 클래스. 직접 사용하기 위한것이 아닌 하위 클래스를 사용하기 위한 것
UICollectionViewFlowLayout
UICollectionViewLayout을 상속받은 라인 기반 레이아웃 시스템
UICollectionViewFlowLayout 기본지식
CollectionView의 스크롤 방향에 따라 셀들을 스크롤 방향의 수직이 되도록 배치. 셀들을 배치 할 때 더이상 배치할 공간이 없으면 다음 줄로 넘어가서 배치. 이렇게 흐르듯이 셀들을 배치해 flowLayout이라함.
Line Spacing : 줄들간의 간격
Inter-Item Spacing: 항목간의 간격
아래 사진은 레이아웃이 Vertical일때. Horizontal일때는 사진을 시계방향으로 90도 돌린 후 좌우 반전하면 됨.
UICollectionViewFlowLayout 구현
prepare()
레이아웃이 무효화 할때마다 호출됨. CollectionView의 경우 CollectionView의 크기가 변경될때마다 레이아웃이 무효화 됨. ex) 디바이스 가로, 세로 모드 변경, 아이패드 앱 크기 조정 등등
sectionInset
섹션에서 콘텐츠를 배치하는데 사용하는 margins
sectionInsetReference
어떤 경계를 기준으로 섹션을 삽입할지 결정. fromSafeArea, fromContentInset, fromLayoutMargins가 있음.
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
guard let cv = collectionView else { return }
self.itemSize = CGSize(width: cv.bounds.inset(by: cv.layoutMargins).size.width, height: 300)
self.sectionInset = UIEdgeInsets(top: self.minimumInteritemSpacing, left: 0.0, bottom: 0.0, right: 0.0)
self.sectionInsetReference = .fromSafeArea
}
}
화면이 변경되면 prepare이 호출. 일정한 layout을 유지 하게 해준다.
UICollectionViewLayout 프로세스
prepare()
레이아웃을 재설정 해야할 때 prepare 호출. 호출은 레이아웃이 무효화 될때 호출되므로 collectionView의 레이아웃에 필요한 계산을 하기 좋음.
collectionViewContentSize
collectionView의 contentSize를 정의한다. 즉 collectionView의 셀들이 모두 들어갔을때의 사이즈를 정의.
layoutAttributesForElements
사용자가 콘텐츠를 스크롤 하거나, 처음 표시할 때 화면에 표시하는데 필요한것들을 알아야 할 때 호출
layoutAttributesForItem
콜렉션 뷰의 셀의 layout을 리턴합니다.
Mosaic 레이아웃 구현
FlowLayout으로 구현하지 못하는 라인베이스의 레이아웃이 아닐경우, UICollectionViewLayout을 채택해서 레이아웃을 구현한다.
prepare에서 각 셀들이 위치할 좌표를 계산하여 cache에 저장합니다.
protocol MosaicLayoutDelegate: AnyObject {
func collectionView(_ collectionView: UICollectionView, heightForImageAtIndexPath indexPath: IndexPath, contentWidth: CGFloat) -> CGFloat
}
class MosaicLayout: UICollectionViewLayout {
weak var delegate: MosaicLayoutDelegate?
fileprivate var numberOfColumns: Int = 2
fileprivate var cellPadding: CGFloat = 6.0
fileprivate var cache: [UICollectionViewLayoutAttributes] = []
fileprivate var contentHeight: CGFloat = 0.0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0.0
}
let insets = collectionView.contentInset
return collectionView.bounds.inset(by: insets).width
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
cache.removeAll()
let columnWidth: CGFloat = contentWidth / CGFloat(numberOfColumns)
var xOffset: [CGFloat] = []
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let imageHeight = delegate?.collectionView(collectionView, heightForImageAtIndexPath: indexPath, contentWidth: columnWidth) ?? 300
let height = cellPadding * 2 + imageHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cache.filter { rect.intersects($0.frame) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}