저번 프로젝트에서 댓글, 대댓글 기능을 구현할 때 CollectionView를 사용하여 구현했다. FlowLayout과 Section에 대해 제대로 이해하지 못하고 있는 것 같아 다시 한 번 짚고 넘어가고자 한다. CollectionView는 매우 유용하지만, 이를 효과적으로 활용하고 구현하기 위해서는 FlowLayout과 섹션에 대한 명확한 이해가 필수적이라고 생각한다. 이번 게시글에서는 이 CollectionView에 대해 깊이 있게 생각해보고 탐구해보고자 한다.
UICollectionViewFlowLayout
A flow layout is a type of collection view layout. Items in the collection view flow from one row or column (depending on the scrolling direction) to the next, with each row containing as many cells as will fit. Cells can be the same sizes or different sizes.
UICollectionViewFlowLayout은 CollectionView Layout의 한 유형이다. CollectionView의 item은 한 행이나 열에서 다음 행으로 Flow하며 배치 된다. 각 행에는 셀들이 들어갈 수 있을 만큼 들어가게 된다. 셀들은 같은 크기일 수도 있고, 다른 크기일 수도 있다.
즉, UICollectionViewFlowLayout은 UICollectionView의 레이아웃을 정의하는 데 사용되는 클래스로, CollectionView의 item들의 배치를 관리하는 역할을 한다. UICollectionViewFlowLayout은 Grid, List, Stackerd 등 다양한 레이아웃 스타일을 구현할 수 있도록 돕는다.
✅ 주요 속성
private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
let flowLayout = UICollectionViewFlowLayout()
// 스크롤 방향 (default vertical)
flowLayout.scrollDirection = .vertical
// 그리드 열 최소 간격 (default 10)
flowLayout.minimumLineSpacing = 30
// 그리드 행 최소 간격 (default 10)
flowLayout.minimumInteritemSpacing = 30
// 각 Cell의 크기 (default w:50, h:50)
flowLayout.itemSize = CGSize(width: 100, height: 100)
// 동적으로 Cell의 크기 계산
flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
// Section에 대한 여백
flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
// header, footer 크기
// 스크롤 방향에 따라 크기에 영향 (수직 방향은 height값만 가지고 계산, 수평 방향은 width값만 가지고 계산)
flowLayout.headerReferenceSize = CGSize(width: 50, height: 50)
flowLayout.footerReferenceSize = CGSize(width: 50, height: 50)
// header, footer 고정 (defalut false)
flowLayout.sectionHeadersPinToVisibleBounds = true
flowLayout.sectionFootersPinToVisibleBounds = true
return flowLayout
}()
1. scrolloDirection
CollectionView의 스크롤 방향을 설정할 수 있다. 기본값은 수직 스크롤이지만, 수평 스크롤로 변경할 수 있다.
2. Item 간격
minimumInterLineSpacing 속성은 그리드 열의 최소 간격이다. 초록색 화살표가 최소 간격이고, 검은색 화살표가 실제 셀 사이의 간격이다.
minimumInteritemSpacing 속성은 그리드 행의 최소 간격이다.
3. sectionInset
sectionInset 속성을 사용하여 섹션의 테두리에 padding을 추가할 수 있다. CollectionView의 가장자리와 섹션 사이에 공간을 추가하는 데 유용하다.
4. itemSize
itemSize 속성을 통해 각 item의 크기를 설정할 수 있다. 이를 통해 고정된 크기의 아이템을 배치할 수 있다.
UICollectionViewDelegateFlowLayout
UICollectionViewFlowLayout의 속성을 직접 지정하는 대신 delegate 메소드를 사용하여 레이아웃을 조정할 수 있다.
굳이 delegate로 지정하는 이유는 무엇일까? ➡️ 유연성과 동적 설정 가능성 때문.
UICollectionViewDelegateFlowLayout을 사용하면 각 아이템, 섹션, 행 등에 대해 더 세밀하고 동적인 제어가 가능하다.
1. 아이템 크기의 동적 설정
collectionView(_:layout:sizeForItemAt:) 메소드를 사용하여 각 아이템의 크기를 동적으로 설정할 수 있다.
➡️ 아이템의 내용이나 상태에 따라 크기를 조절할 때 유용하다.
2. 섹션별 inset 설정
collectionView(_:layout:insetForSectionAt:) 메소드를 사용하여 섹션마다 다른 인셋을 적용할 수 있다.
➡️ 섹션별로 다른 레이아웃을 적용할 때 유용하다.
3. 아이템 간 간격 설정
collectionView(_:layout:minimumInteritemSpacingForSectionAt:), collectionView(_:layout:minimumLineSpacingForSectionAt:) 메소드를 사용하여 섹션별로 다른 간격을 설정할 수 있다.
즉, UICollectionViewDelegateFlowLayout를 사용하면 다양한 레이아웃 요구사항을 만족시키기 위해 각 아이템과 섹션에 대해 동적으로 크기와 간격을 설정할 수 있다. 이는 레이아웃이 고정된 것이 아니라, 데이터나 상태에 따라 변할 필요가 있는 경우에 특히 유용하다.
구현 해보기
인스타그램의 나의 피드 그리드를 구현해보자.
import UIKit
class ViewController: UIViewController {
private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
return flowLayout
}()
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.id)
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
setConstraint()
collectionView.delegate = self
collectionView.dataSource = self
}
private func setConstraint() {
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100).isActive = true
}
}
수직 스크롤 방향의 UICollectionViewFlowLayout을 초기화하고, collectionViewFlowLayout을 사용하여 UICollectionView를 초기화 한다. 그리고 제약조건을 설정해준다. 추가적으로 collectionView를 제어하기 위해 delegate과 dataSource를 설정한다.
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalSpacing = 2 * 1 // leading + trailing + (numberOfItemsInRow - 1) * spacing
let width = (UIScreen.main.bounds.width - CGFloat(totalSpacing)) / 3.0
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.id, for: indexPath) as? CollectionViewCell else {
return UICollectionViewCell()
}
return cell
}
}
sizeForItemAt 메소드를 이용해서 각 셀의 크기를 설정한다. 위 코드에서는 한 행에 3개의 셀을 표시하도록 설정했다.
✅ 각 기기마다 크기가 달라질 수 있기 때문에 UIScreen.main.bounds.width를 이용해서 너비를 구했다.
✅ totalSpacing은 collectionView의 leading과 trailing이 각각 0이고, 각 행에 아이템이 3개이므로 0 + 0 (3 - 1) * 1 / 3.0이다
✅ 따라서 아이템의 너비는 전체 화면 너비 - 총 간격 / 3이다.
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalSpacing = 10 + 10 + 2 * 5 // leading + trailing + (numberOfItemsInRow - 1) * spacing
let width = (UIScreen.main.bounds.width - CGFloat(totalSpacing)) / 3.0
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
}
만약 collectionView의 leading과 trailing이 각각 10, -10이고 Cell사이의 간격이 5이면, 위와 같이 수식을 수정해준다.
'Swift > UIkit' 카테고리의 다른 글
[UIKit] UITableView의 Section에 대해 알아보자. (0) | 2024.06.21 |
---|---|
[UIKit] frame과 bounds를 알아보자. (0) | 2024.06.20 |
[UIKit] Button의 이벤트를 addAction 메소드를 이용해서 처리해보자. (iOS 14+) (0) | 2024.04.30 |
[UIkit] 문제 해결 - contentView ?!!! 💣 (0) | 2023.03.30 |
[UIkit] CollectionView와 tableView를 이용해서 커스텀 레이아웃 만들기 (0) | 2023.03.28 |