UICollectionViewLayout, UICollectionViewFlowLayout

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]
    }

}