[iOS] UICollectionViewLayout 설정

2020. 12. 6. 21:31iOS

반응형

UICollectionView을 자주 사용하실 것 같은데 UITableView와 가장 크게 다른 점은 커스텀하게 Layout을 설정할 수 있는 프로퍼티가 있다는 것 같아요.

 

기본적으로 2가지 방법으로 Layout을 설정할 수 있는데, 상황에 따라 다르게 사용할 수 있어요.

2가지 방법으로는

✔️ UICollectionViewFlowLayoutDelegate 활용
✔️ UICollectionViewFlowLayout 객체 활용

이렇게 2가지 방법으로 설정할 수 있다.

 

UICollectionViewFlowLayout 객체를 활용하는 경우는 엄청 복잡한 레이아웃을 커스텀해야하는 경우나 간단하게 초기 값을 설정하기 위해 사용했어요.

 

UICollectionViewFlowLayoutDelegate 같은 경우는 마찬가지로 데이터를 이용해서 분기처리로 레이아웃을 설정해야하는 경우에 사용했어요.

 

 

UICollectionViewFlowLayout 활용

 

한 번 간단히 어떤 방법으로 사용했는지 예제를 보면서 알아볼게요.

 

먼저, UICollectionViewFlowLayout 객체를 이용하는 경우입니다.

var homeCollectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .vertical
    layout.minimumLineSpacing = 36
    layout.sectionInset = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
    layout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width - 20*2, height: 200)

    let collectionView = UICollectionView(frame: .zero,
                                          collectionViewLayout: layout)


    return collectionView
}()

다음과 같이 간단하게 초기 값을 설정하는 경우에는 UICollectionVIewLayout 객체를 이용해서 프로퍼티를 설정하는 방법으로 설정할 수 있어요. Delegate을 사용하는 것보다 훨씬 간단하겠죠⁉️

 

UICollectionViewFlowLayout 객체를 Custom해서 상속받는 방식으로도 사용할 수 있어요.

 

헤더가 Scroll에 따라 늘어나는 헤더를 구현할 때 썻던 예제로 확인해볼게요.

class StretchHeaderLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)
        
        layoutAttributes?.forEach { attributes in
            if attributes.representedElementKind == UICollectionView.elementKindSectionHeader {
                guard let collectionView = collectionView else { return }
                let contentOffsetY = collectionView.contentOffset.y
                
                if contentOffsetY > 0 { return }
                let width = collectionView.frame.width
                let height = attributes.frame.height - contentOffsetY
                attributes.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
            }
        }
        return layoutAttributes
    }
    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}

class ViewController: UIViewController {
    @IBOutlet weak var stickyCollectionView: UICollectionView!
	let stickyFlowLayout = StretchHeaderLayout()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        stickyCollectionView.setCollectionViewLayout(stickyFlowLayout, animate, true)
    }

}

다음과 같이 UICollectionViewFlowLayout의 메소드를 오버라이드해서 커스텀하게 레이아웃을 설정할 수 있어요.

 

그럼 마지막에 저 UICollectionViewFlowLayout의 메소드와 불리는 호출 주기를 알아보고 우선 Delegate을 활용한 방법 먼저 알아볼게요.

 

 

UICollectionViewFlowLayoutDelegate 활용

 

어떤 ViewController에 UICollectionViewFlowDelegate을 채택해서 사용하는 방법입니다.

 

예제로 볼게요.

class ViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    
	override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let estimateHeight = (collectionView.frame.height - 100) / 2
        let estimateWidth = (collectionView.frame.width - collectionView.frame.width / 14.43 - (collectionView.frame.width / 5.86)) / 2
        return CGSize(width: estimateWidth, height: estimateHeight)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return collectionView.frame.width / 14.43
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 29
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: (collectionView.frame.width / 11.72), bottom: 70, right: (collectionView.frame.width / 11.72))
    }
}

이렇듯 사용할 ViewController에서 해당 프로토콜을 채택해서 데이터에 맞게 CollectionView의 레이아웃을 커스텀 할 수 있어요.

 

아무래도 쓸 코드도 조금 많아지긴해서 위에 UICollectionViewFlowLayout 객체를 이용하는 것보다는 복잡하죠..⁉️

하지만 데이터에 맞게 커스텀 해야할 경우에는 사용하기 좋은 것 같아요. (약간 중간 난이도 정도..⁉️)

 

이렇게 Delegate을 이용할 때는 위의 코드와 같이 사용할 수 있어요!

 

마지막으로 UICollectionViewFlowLayout의 메소드와 호출시기에 대해 알아볼게요~

 

 

UICollectionViewLayout 메소드

 

아래 그림이 CollectionView와 UICollectionViewFlowLayout 객체가 상호작용하는 과정입니다.

그림을 보면 처음 CollectionView가 prepareLayout을 통해 레이아웃의 초기값을 설정하고 이후 CollectionView의 크기를 받고 각 Cell의 크기들을 지정하고 받는 것을 알 수 있죠⁉️

 

그렇다면 그러한 동작들은 어느 메소드에서 이루어 질까요.

 

하나씩 볼게요.

  • prepare() : 초기에 CollectionView의 레이아웃을 설정할 때 호출됩니다. 즉, 사이즈를 위한 어떤 셋팅이 필요할 때, 이 메소드를 override해서 사용할 수 있습니다. (CollectionView의 초기 설정이 아닌 레이아웃 연산이 일어나기 전입니다.)
  • layoutAttributesForElements(rect:) -> [UICollectionViewLayoutAttributes]? : CollectionView 안의 모든 요소에 대한 Layout 요소들을 리턴합니다. (현재 화면에 보이는 요소)
  • layoutAttributesForItem(indexPath:) -> UICollectionViewLayoutAttributes? : CollectionView에 요구한 IndexPath의 Layout 요소들을 리턴합니다.
  • shouldInvalidateLayout(newBounds:) -> Bool : Bounds에 변화가 있을 때마다, 함수를 호출할 지 결정합니다. InvalidateLayout(with:)을 호출합니다.

 

아직 UICollectionViewFlowLayout을 활용해서 복잡한 뷰는 구현해보지 않았는데, 다음에는 이 객체를 이용해서 Pinterest Layout을 구현하는 연습을 해볼게요.

 

혹시 틀린 내용이나 궁금한게 있으면 말해주세요~~

반응형