LBTATools 라이브러리를 사용하여 UICollectionView에 Header를 손쉽게 달아보자.

  1. 일반적인 Header 등록 방법
  2. LBTATools 를 사용한 Header 만들기
  3. Header에 collectionViewController 넣기
  4. insetForSection을 사용한 세밀한 margin 조절
  5. Header 안의 collectionViewController’s Action

1. 일반적인 Header 등록 방법

UICollectionView에서 일반적으로 Header를 등록하는 방법은 다음과 같다. header와 같은 view들은 collection view에서 supplementary view로 분류가 되며 이러한 supplementary view를 추가하기 위해서는 register라는 collectionView의 method를 이용해서 해당 클래스를 등록해야한다.

Document

register(_:forSupplementaryViewOfKind:withReuseIdentifier:)

Registers a class for use in creating supplementary views for the collection view.

Declaration

func register(_ viewClass: AnyClass?, forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String)

Parameters

  • viewClass

    The class to use for the supplementary view.

    UICollectionReusableView 로 만들어진 class를 넣어주자.

  • elementKind

    The kind of supplementary view to create. This value is defined by the layout object. This parameter must not be nil.

    UICollectionView.elementKindSectionHeader 로 입력

  • identifier

    The reuse identifier to associate with the specified class. This parameter must not be nil and must not be an empty string.

    익숙한 identifier

Discussion

Prior to calling the dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)method of the collection view, you must use this method or the register(_:forSupplementaryViewOfKind:withReuseIdentifier:) method to tell the collection view how to create a supplementary view of the given type. If a view of the specified type is not currently in a reuse queue, the collection view uses the provided information to create a view object automatically.

If you previously registered a class or nib file with the same element kind and reuse identifier, the class you specify in the viewClass parameter replaces the old entry. You may specify nil for viewClass if you want to unregister the class from the specified element kind and reuse identifier.

register in viewDidLoad

사용된 parameters는 모두 예시. 사용할 headerView를 register 해주는 과정.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.register(UICollectionReusableView, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "headerId")
      ...

viewforsupplementary

register한 headerView(supplementary view)를 duqueuing 작업. dequeuing할 때에는 type casting까지 꼭 해준다.

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        <#code#>
      let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! H
        return header
    }

refsizeforheader

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        <#code#>
    }

2. LBTATools 를 사용한 Header 만들기

LBTA github

위에 필수적인 과정을 LBTATools 라이브러리를 사용하면 매번 입력할 필요가 없다. 기존 UICollectionViewController 대신에 LBTAListHeaderController 를 사용하면 된다.

class

LBTAListHeaderController를 선언할 때에는 LBTAListController에서 선언할 때 꼭 필요한 LBTAListCell과 Cell에 들어갈 타입인 U, 그리고 Header로 사용할 UICollectionReusableView 이렇게 3개가 필요하다.

	open class LBTAListHeaderController<T: LBTAListCell<U>, U, H: UICollectionReusableView>: UICollectionViewController

Cell의 구성 요소 U는 처음에는 UIColor를 사용하면 기본적으로 잘 rendering 되는지 그리고 요소들의 기본 위치를 정하기에 매우 훌륭하다.

세부 구현 내용

    open var items = [U]() {
        didSet {
            collectionView.reloadData()
        }
    }
    
    fileprivate let cellId = "cellId"
    fileprivate let headerId = "headerId"
    
    override open func viewDidLoad() {
        super.viewDidLoad()
        collectionView.backgroundColor = .white
        
        collectionView.register(T.self, forCellWithReuseIdentifier: cellId)
        collectionView.register(H.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId)
    }
    
    /// ListHeaderController automatically dequeues your T cell and sets the correct item object on it.
    override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! T
        cell.item = items[indexPath.row]
        cell.parentController = self
        return cell
    }
    
    /// Override this to manually set up your header with custom behavior.
    open func setupHeader(_ header: H) {}
    
    override open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! H
        setupHeader(header)
        return header
    }

Brian형님 멋져요 :clap::clap::clap:

3. Header에 collectionViewController 넣기

멋진 앱이라면 하나의 ViewController에 여러개 collectionView가 있다!

Header 안에 다른 collectionViewController를 넣기 위해 먼저 dummy controller를 넣자. UIViewController를 사용해 dummy로 사용가능하다.

dummy에 들어갈 ViewController는 LBTAListController를 사용할 것이다. UICollectionViewController를 사용한다면 dequeuing 과정 등이 코드를 조금 더럽히기 때문이다.

Cell크기 조절을 위해 UICollectionViewDelegateFlowLayout 프로토콜을 사용

Document

UICollectionViewDelegateFlowLayout

The UICollectionViewDelegateFlowLayout protocol defines methods that let you coordinate with a UICollectionViewFlowLayout object to implement a grid-based layout. The methods of this protocol define the size of items and the spacing between items in the grid.

마지막 줄 : 이 protocol은 item들의 size와 spacing을 정의하는 메소드들을 제공한다.

scroll direction

in viewDidLoad

layout을 바꾼다고 생각하자.

        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
        }

stackView에서 특정 요소에만 padding이나 margin을 주고 싶다면, stackView 안에서 또 stackView로 묶는다면 빠르고 효율적이다.

4. insetForSection을 사용한 세밀한 margin 조절

UITableView에서 section은 Header, Cells, Footer까지 포함한 영역을 section이라고 명한다. 하지만 UICollectionView에서 insetForSection은 조금 다르다. 문서를 참고해보면 다음과 같다.

Document

collectionView(_:layout:insetForSectionAt:)

Asks the delegate for the margins to apply to content in the specified section.

Declaration

optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets

Parameters

  • collectionView

    The collection view object displaying the flow layout.

  • collectionViewLayout

    The layout object requesting the information.

  • section

    The index number of the section whose insets are needed.

Return Value

The margins to apply to items in the section.

Discussion

If you do not implement this method, the flow layout uses the value in its sectionInset property to set the margins instead. Your implementation of this method can return a fixed set of margin sizes or return different margin sizes for each section.

Section insets are margins applied only to the items in the section. They represent the distance between the header view and the first line of items and between the last line of items and the footer view. They also indicate the spacing on either side of a single line of items. They do not affect the size of the headers or footers themselves.

하지만 Section insets는 items에만 적용되는 margins이기에 margins은 header view와 첫 item의 거리, 마지막 item과 footer view의 거리를 나타낸다. 또한 items 간의 공간도 나타낸다. Section insets는 header, footer, other items의 size에 영향을 끼치지 않는다.

margin은 바깥, padding은 안쪽!

5. Header 안의 collectionViewController’s action

기존의 collectionViewController에서 didSelectItem 메소드를 이용하여 navigationController에 ViewController를 push하여 action을 취하였다.

하지만 header view에 추가된 collectionVC는 navigationController가 없다. 똑같은 action을 취하기 위해서 header의 collectionVC가 superClass를 reference property로 가지고 있고 이를 이용해 superClass에 함수를 만들어 이를 호출시키게 하는 방법을 사용한다.

이때 superClass (LBTAListHeaderController)에 setupHeader 메소드를 override한 후에 이용하면 손쉽게 reference를 연결시킬 수 있다.

setupHeader

controller property를 self로 연결해준다.

    override func setupHeader(_ header: MatchesHeader) {
        header.matchesHorizontalController.someController = self
    }

세부 구현 내용

setupHeader는 viewForSupplementaryElementOfKind에서 header view를 return하기 전에 실행되는 메소드이다.

    open func setupHeader(_ header: H) {}
    
    override open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! H
        setupHeader(header)
        return header
    }

a function for action

didSelect에서 실행시킬 함수를 만들어 item을 매개변수로 받는다.

    func didSelectItemFromHeader(item: Item) {
        let otherController = OtherController(item: item)
        navigationController?.pushViewController(otherController, animated: true)
    }

위 함수를 header view의 collectionVC의 didSelectItemAt 에서 실행시켜주면 된다. invoke!

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let item = items[indexPath.item]
        someController?.didSelectItemFromHeader(item: item)
    }

요즘에는 계속 LBTATools 라이브러리를 사용하여 LBTAListController를 많이 사용해서 기존의 UICollectionViewController 를 통해 만드는 방법을 점차 잊어버리고 있는 기분이다. 앞으로는 definition을 계속 참조하면서 basic을 잊지 않도록 하는게 좋을 것 같다. 하지만 LBTATools 는 혁명이다.

태그: ,

카테고리:

업데이트:

댓글남기기