본문 바로가기

학습 노트/iOS (2021)

100. Supplementary View

Supplementary View

Collection View의 Headr와 Footer를 합쳐서 Supplementary View라고 부른다.
Flow Layout에서는 섹션의 시작부분과 마지막 부분에 각각 표시된다.
Layout 속성과 Delegate 패턴을 통해 크기를 설정할 수 있지만, 스크롤 방향에 따라 제약을 받는다.

  • Vertical
    너비가 Collection View의 너비로 고정되고 높이만 설정할 수 있다.
  • Horizontal
    높이가 Collection View의 높이로 고정되고, 너비만 설정할 수 있다.

단, Custom Layout을 직접 적용하면 크기와 위치를 자유롭게 지정할 수 있다.

Supplementary View는 재사용 메커니즘을 사용한다.
따라서 필요할 때 Collection View에 요청해야 한다.
Flow Layout에서 Supplementary View를 요청할 때는 재사용 식별자와 Header와 Footer를 구분하는 문자열을 전달해야 한다.
Header는 UICollectionElementKindSectionHeader, Footer는 UICollectionElementKindSectionFooter를 각각 전달한다.

//
//  SupplementaryViewController.swift
//  CollectionViewPractice
//
//  Created by Martin.Q on 2021/10/13.
//

import UIKit

class SupplementaryViewController: UIViewController {
	
	@IBOutlet weak var collectionView: UICollectionView!
	
	let list = MaterialColorDataSource.generateMultiSectionData()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
	}
	
	
}

extension SupplementaryViewController: UICollectionViewDataSource {
	func numberOfSections(in collectionView: UICollectionView) -> Int {
		return list.count
	}
	func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
		return list[section].colors.count
	}
	
	func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
		let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
		cell.contentView.backgroundColor = list[indexPath.section].colors[indexPath.row]
		
		return cell
	}


}

사용할 씬과 코드는 위와 같다.

Collection View의 Attribute Inspector에는 Section Header와 Footer를 활성화할 수 있는 속성이 존재한다.

원하는 것을 활성화 하면 씬에 Prototype이 추가된다.
Header는 위와 같이 Interface builder에서 위와 같이 추가하고,
Footer는 코드로 추가해 본다.

Interface Builder로 Section Header 추가하기

Section Header의 Identifier는 header로 설정했다.
이후에는 해당 Identifier로 Header를 요청할 수 있다.

Header에 Label을 추가하고, 왼쪽을 기준으로 표시되고, 수직으로 가운데에 위치하도록 제약을 추가했다.

새로 추가한 Label에 접근하기 위해선 outlet으로 연결해야 하는데,
이는 셀과 동일하게 새로운 Class를 생성하고 해당 클래스에 연결하는 방식으로 진행한다.

이때 UICollectionReusableView를 상속받아야 함을 명심하자.
이후 SectionHeader의 Custom Class로 지정한다.

//
//  SectionHeaderCollectionReusableView.swift
//  CollectionViewPractice
//
//  Created by Martin.Q on 2021/10/13.
//

import UIKit

class SectionHeaderCollectionReusableView: UICollectionReusableView {
	@IBOutlet weak var sectionHeaderLabel: UILabel!


}

이후 클래스에 outlet으로 연결한다.

extension SupplementaryViewController: UICollectionViewDataSource {
	func numberOfSections(in collectionView: UICollectionView) -> Int {
		return list.count
	}
	func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
		return list[section].colors.count
	}
	
	func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
		let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
		cell.contentView.backgroundColor = list[indexPath.section].colors[indexPath.row]
		
		return cell
	}
	func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
	
	}

}

다시 원래의 파일로 돌아와 dataSource에 collectionView(viewForSupplementaryElementOfKind:)메소드를 추가한다.
Collection View에 Supplementary View를 표시하려면 해당 메소드를 반드시 구현해야 한다.
두 번째 파라미터인 kind로는 Supplementary를 구별하는 문자열이 전달된다.

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
	let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath)
}

재사용 셀을 구현하는 것과 비슷한 방식으로 구현한다.
collectionView의 dequeReusableSupplementaryView 메소드를 통해 재사용 SupplementaryView를 생성한다.

첫 번째 파라미터인 ofKind로 Supplement View의 종류를 전달하고,
두 번째 파라미터인 withReuseIdentifier로 Section Header에 부여했던 Identifier를 전달한다.
indexPath도 그대로 전달한다.

파라미터들로 전달된 재사용 뷰가 존재한다면 반환하고, 존재하지 않는다면 프로토타입을 통해 새로 생성해 반환한다.
이러한 View의 반환형은 UICollectionReuableView이다.
프로토타입에 존재하는 outlet에 접근하려면 실제 형식으로 타입 캐스팅해야 한다.

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
	let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! SectionHeaderCollectionReusableView
	
	header.sectionHeaderLabel.text = list[indexPath.section].title
	
	return header
}

SectionHeaderCollectionReusableView로 타입캐스팅 해 Label을 설정하고 이를 반환한다.

성공적으로 적용됐다.

단, Table View와는 달리 스크롤 하면 셀들과 함께 움직인다.
이번엔 Table View와 동일한 Sticky Header를 구현해 본다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
		layout.sectionHeadersPinToVisibleBounds = true
	}
}

collectionViewLayout을 UICollectionViewFlowLayout으로 타입 캐스팅 한 뒤,
sectionHeadersPinToVisibleBounds를 true로 설정하면 Sticky Header를 사용할 수 있다.
마찬가지의 방법으로 sectionFooterPinToVisibleBounds를 true로 설정해 Sticky Footer를 사용할 수 있다.

간단하게 Sticky Header가 구현됐다.

코드로 Section Footer 추가하기.

이번엔 아직 구현하지 않은 Footer를 코드로 구현해 본다.
Interface Builder를 통해 작성한 Supplementary View는 시각적으로 디자인을 즉시 확인할 수 있고,
Collection View에 등록하는 과정도 직접 구현할 필요가 없다.
하지만 작성한 프로토타입을 다른 Collection View와 공유할 수 없다는 단점이 존재한다.
Prototype Cell이나 Supplementary View를 여러 Collection View에서 공유하고 싶다면
별도의 nib 파일로 구현하거나 코드로 구현해야 한다.

새로 생성하는 클래스 파일도 UICollectionReusableView를 상속받고 있어야 한다.

//
//  SectionFooterCollectionReusableView.swift
//  CollectionViewPractice
//
//  Created by Martin.Q on 2021/10/13.
//

import UIKit

class SectionFooterCollectionReusableView: UICollectionReusableView {

	var sectionFooterLabel: UILabel!
}

Label에 접근할 속성을 생성하고,

//
//  SectionFooterCollectionReusableView.swift
//  CollectionViewPractice
//
//  Created by Martin.Q on 2021/10/13.
//

import UIKit

class SectionFooterCollectionReusableView: UICollectionReusableView {
	
	var sectionFooterLabel: UILabel!
	
	private func setup() {
		let label = UILabel(frame: bounds)
		label.translatesAutoresizingMaskIntoConstraints = false
		label.font = UIFont.boldSystemFont(ofSize: 20)
		addSubview(label)
		
		label.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1.0).isActive = true
		label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
		
		sectionFooterLabel = label
	}
}

setup 메소드를 생성해 UI를 구현한다.
기본적으로 header와 동일한 UI이지만, 배경색이 흰색이고, 글씨가 검은색이다.

//
//  SectionFooterCollectionReusableView.swift
//  CollectionViewPractice
//
//  Created by Martin.Q on 2021/10/13.
//

import UIKit

class SectionFooterCollectionReusableView: UICollectionReusableView {
	
	var sectionFooterLabel: UILabel!
	
	private func setup() {
		let label = UILabel(frame: bounds)
		label.translatesAutoresizingMaskIntoConstraints = false
		label.font = UIFont.boldSystemFont(ofSize: 20)
		addSubview(label)
		
		label.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1.0).isActive = true
		label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
		
		sectionFooterLabel = label
	}
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		setup()
	}
	
	required init?(coder: NSCoder) {
		super.init(coder: coder)
		setup()
	}
}

이후 생성자를 오버라이딩해 setup 메소드를 호출하도록 한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
		layout.sectionHeadersPinToVisibleBounds = true
	}
	
	collectionView.register(SectionFooterCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footer")
}

다시 원래의 코드로 돌아와 Collection View에 Footer를 등록한다.
collectionView의 register 메소드를 사용하며,

첫 번째 파라미터로 사용할 클래스를 전달하고,
두 번째 파라미터로는 Supplementary View를 구분하는 문자열을 전달한다.
Flow Layout을 사용하는 경우 UICollectionView의 elementKindSectionFooter로 정해져 있다.
세 번째 파라미터로는 재사용 뷰로 호출할 식별자를 전달한다. footer로 설정했다.

만약 앱 어딘가의 다른 Collection View에서 같은 디자인의 Footer를 사용해야 한다면,
위와 같은 방법을 통해 사용할 수 있다.
디자인을 수정할 경우에도 클래스파일이나 nib파일 하나만 수정하면 모두 적용되기 때문에 유지보수에도 유리하다.

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
	let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! SectionHeaderCollectionReusableView
	
	header.sectionHeaderLabel.text = list[indexPath.section].title
	
	return header
}

dataSource에서 이전에 작성했던 ColelctionView(viewForSupplementaryElementOfKind:)에서는 header만 반환하고 있다.
이것을 viewForSupplementaryElementOfKind에 따라 분기해 Header와 Footer를 반환하도록 수정한다.

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
	switch kind {
	case UICollectionView.elementKindSectionHeader:
		let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! SectionHeaderCollectionReusableView
		
		header.sectionHeaderLabel.text = list[indexPath.section].title
		
		return header
	case UICollectionView.elementKindSectionFooter:
		let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "footer", for: indexPath) as! SectionFooterCollectionReusableView
		
		footer.sectionFooterLabel.text = list[indexPath.section].title
		
		return footer
	default:
		fatalError("Unsupported kind")
	}
}

이렇게 되면 전달되는 문자열에 따라서 분기해 적당한 View를 반환하게 된다.

하지만 실행해 보면 Footer는 존재하지 않는다.

Collection View의 Size Inspector를 확인해 보면 Footer의 사이즈가 설정되어있지 않다.
Footer를 정상적으로 표시하려면 0이상의 크기로 설정해야 한다.

if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
	layout.sectionHeadersPinToVisibleBounds = true
	layout.footerReferenceSize = CGSize(width: 50, height: 50)
}

ViewDidLoad에서 footerReferenceSize를 통해 크기를 설정 후 다시 확인해 보자.

의도한 대로 Footer 가 표시된다.

코드로 Supplementary View를 작성하면 Collection View에 직접 등록해야 하고, 크기를 직접 설정해 줘야 한다.

Supplementary API

collectionView.supplementaryView(forElementKind: String, at: IndexPath)

해당 메소드는 지정된 위치에 있는 Supplementary View에 접근할 때 사용한다.
전달된 IndexPath와 동일한 위치에 존재하고, ElementKind와 동일하다면 해당 View를 반환하고, 아니면 nil을 반환한다.

collectionView.indexPathsForVisibleSupplementaryElements(ofKind: String)

해당 메소드는 Collection View에 표시되어있는 Supplementary View 중에서 Kind가 일치하는 View의 IndexPath 목록을 반환한다.

collectionView.visibleSupplementaryViews(ofKind: String)

해당 메소드는 kind가 일치하는 Supplementary View 중에 Collection View에 표시되고 있는 View의 목록을 반환한다.

Header와 Footer의 크기는 Delegate 패턴으로도 설정할 수 있다.
이전처럼 Layout 속성에서 설정하거나 Interface Builder로 설정하면 모든 Header와 Footer에 동일하게 적용된다.
Section마다 별도의 크기를 지정하고 싶다면 UICollectionViewDelegateFlowLayout 프로토콜을 구현하고,
원하는 크기를 반환한다.

extension SupplementaryViewController: UICollectionViewDelegateFlowLayout {
	func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
		
	}
	func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
		
	}
}

 

해당 메소드들은 각각 Header와 Footer의 크기를 반환한다.