- RxDataSources 5.0.2
- RxSwift 6.5.0
- SnapKit 5.0.1
- Section, SectionHeader(빨간색)
- Section을 나눠서 각 Section의 Header를 설정
- dynamic Cell Height(파란색)
- Cell내부에 있는 label크기에 따라 cell의 height을 조정
RxDataSource를 이용하기 때문에 SectionModel을 구현해준다.
Section은 총 2개며 각 Section마다 필요한 data는 title 파라미터 이며 String타입 이다.
import RxDataSources
enum MySectionModel: SectionModelType {
typealias Item = MySectionItem
case section1(items: [MySectionItem])
case section2(items: [MySectionItem])
var items: [MySectionItem] {
switch self {
case .section1(items: let items):
return items
case .section2(items: let items):
return items
}
}
init(original: MySectionModel, items: [Item]) {
switch original {
case .section1(items: _):
self = .section1(items: items)
case .section2(items: _):
self = .section2(items: items)
}
}
}
enum MySectionItem {
case section1Item(title: String)
case section2Item(title: String)
}
ViewController측에서 dataSource를 이용해 collectionView의 item을 채울 것 이므로 [MySectionModel]
타입이 필요하다.
필요한 데이터는 ViewModel에서 방출해 주도록 한다.
첫번째 section에는 1, 2, 3 이 방출되고, 두번째 section에는 a, b, c가 방출된다.
func bind() {
Observable.combineLatest(self.section1Relay, self.section2Relay)
.map{ (a, b) in
// Q) 이거 좀 구려 보이는데 반복문으로 깔끔하게 하는 방법 없을까?
var arr = [MySectionModel]()
let item = a.map{ MySectionModel.Item.section1Item(title: $0) }
let item2 = b.map{ MySectionModel.Item.section2Item(title: $0) }
let mySectionModel1 = MySectionModel.section1(items: item)
let mySectionModel2 = MySectionModel.section2(items: item2)
arr.append(mySectionModel1)
arr.append(mySectionModel2)
return arr
}
.bind(to: self.sections)
.disposed(by: disposeBag)
self.section1Relay.accept(["1", "2", "3"])
self.section2Relay.accept(["a", "b", "c"])
}
CollectionView의 layout을 어떻게 표출할지에 대한 정의가 필요하다.
ViewController에서 UICollecitonViewFlowLayout
를 채택해서 delegate를 구현해도 되고 layout.itemsSize
등을 이용해서 layout을 수정을 할 수 있지만
UICollecitonViewFlowLayout
을 채택한 class를 새로 만들도록 한다.
- layoutAttributesForElements(in:)
- 호출 시점(정확하지 않음): collectionView가 그려질 때
- 요약: parameter rect내의 모든 cell과 view에 대한 layout 정보를 가져온다.
- return: cell또는 view에 대한 layout정보를 나타내는 객체의 배열
UICollectionViewLayoutAttributes
이다. 기본 전달값은 nil이다.
- layoutAttributesForItem(at:)
- 호출 시점(정확하지 않음): collectionView가 그려질 때 (layoutAttributesForElements(in:) 이후 각 cell 마다 호출, 현재 cell이 6개이므로 6번 호출)
- 요약: 각 cell의 layout 정보를 가져온다.
- return: cell에 적용할 정보가 포함된 layout 객체
두 개의 override 메서드를 이용한다.
final class MultiSectionCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
print("layoutAttributesForElements")
// super를 호출하여 layoutAttributes의 값을 획득합니다.
let layoutAttributeObjects = super.layoutAttributesForElements(in: rect)
layoutAttributeObjects?.forEach { layoutAttributes in
// cell의 layoutAttributes만 조정하기 위해 representedElementCategory Property를 이용해 cell인지 체크합니다.
if layoutAttributes.representedElementCategory == .cell {
// cell인 경우, 그 아이템의 indexPath를 이용해 layoutAttributesForItem을 호출하고 각 cell 마다의 SelfSizing된 frame을 획득합니다.
if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
layoutAttributes.frame = newFrame
}
}
}
return layoutAttributeObjects
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
print("layoutAttributesForItem")
guard let collectionView = collectionView else { return nil }
// indexPath를 통해 해당 cell의 layoutAttributes를 얻어옵니다.
guard let layoutAttirbutes = super.layoutAttributesForItem(at: indexPath) else { return nil }
// collectionView 절반을 차지하도록 width를 계산합니다.
layoutAttirbutes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width
return layoutAttirbutes
}
}
그리고 난 후 Cell을 만들어 준다.
주의 할점은 cell 내부에 들어갈 content들은 모두 contentView에 넣어줘야 한다.
self.contentView.addSubview(label)
그리고 constraint를 걸어줘서 화면에서 어떻게 나오는 지 보도록 한다.
label.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalToSuperview().offset(30)
make.bottom.equalToSuperview().offset(-30)
}
마지막으로 preferredLayoutAttributesFitting
을 구현해준다.
- preferredLayoutAttributesFitting
- 호출시점:
- 요약: 수정된 layoutAttributes를 적용하는 함수
- 이 메서드의 기본 구현은 자체 크기 조정 셀의 변경 사항을 수용하도록 크기 값을 조정합니다. 서브클래스는 이 메서드를 재정의하고 다른 레이아웃 속성을 조정하는 데 사용할 수도 있습니다. 이 메서드를 재정의하고 셀 크기 조정을 원하는 경우
super
먼저 호출하고 반환된 속성을 직접 수정합니다. - 위의 FlowLayout에서 작성한 함수들을 통해 최종적으로 계산된 layoutAttributes를 적용하는 함수
- 이 메서드의 기본 구현은 자체 크기 조정 셀의 변경 사항을 수용하도록 크기 값을 조정합니다. 서브클래스는 이 메서드를 재정의하고 다른 레이아웃 속성을 조정하는 데 사용할 수도 있습니다. 이 메서드를 재정의하고 셀 크기 조정을 원하는 경우
- return: cell에 적용할 최종 attributes
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
print("preferredLayoutAttributesFitting")
super.preferredLayoutAttributesFitting(layoutAttributes)
// Q) 이거 height를 0으로 주던데 왜??
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
layoutAttributes.frame.size
= contentView.systemLayoutSizeFitting(
targetSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
return layoutAttributes
}
- collectionView layout설정
- collectionView의 cell, supplementaryView 등록
- section footer, header 크기 설정
- collectionView cell의 각 사이를 정해주는 UIEdgeInsets 구현
- collectionView의 자세한 정보를 담고 있는 dataSource를 구현
- collectionView item 구현
collectionView의 layout을 설정해준다.
private lazy var collectionView: UICollectionView = {
print("collectionView init")
let layout = MultiSectionCollectionViewFlowLayout()
// scroll은 새로 방향
layout.scrollDirection = .vertical
// 자동 사이즈
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
return collectionView
}()
collectionView의 cell, supplementaryView 등록
collectionView.register(Section1Cell.self, forCellWithReuseIdentifier: Section1Cell.ID)
collectionView.register(Section2Cell.self, forCellWithReuseIdentifier: Section2Cell.ID)
collectionView.register(Section1HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: Section1HeaderView.ID)
footer의 크기는 0, header의 크기는 50으로 지정해준다.(section별로 나누고 싶다면 section parameter를 이용한다.)
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int
) -> CGSize {
return CGSize(width: self.view.frame.size.width, height: 50)
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int
) -> CGFloat {
return 0.0
}
그리고 cell 사이의 간격은 0으로 지정해준다.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int
) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
마지막으로 collectionView에 어떤 내용들이 담겨있는지를 정의해준다. dataSource 구현
private static var dataSource: MySectionDataSource {
print("dataSource init")
// cell을 먼저 구현한다.
let configureCell: (
CollectionViewSectionedDataSource<MySectionModel>,
UICollectionView,
IndexPath,
MySectionModel.Item
) -> UICollectionViewCell = { dataSource, collectionView, indexPath, item in
print("configureCell")
// 각 section별로 나눠야 하니까 dataSource[indexPath]를 switch에 넣는다.
switch dataSource[indexPath] {
// 첫번째 section안에 들어갈 cell 내용
// 여기 title은 이전에 구현했던 SectionModelItem에서 받아오는 값이다.
case .section1Item(title: let title):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section1Cell.ID, for: indexPath) as! Section1Cell
cell.configure(title: title)
return cell
case .section2Item(title: let title):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section2Cell.ID, for: indexPath) as! Section2Cell
cell.configure(title: title)
return cell
}
}
// supplementrayView(header, footer)를 구현
let supplementrayView: (
CollectionViewSectionedDataSource<MySectionModel>,
UICollectionView,
String,
IndexPath
) -> UICollectionReusableView = { dataSource, collectionView, kind, indexPath in
print("supplementrayView")
// 각 section별로 나눠야 하니까 dataSource[indexPath]를 switch에 넣는다.
switch dataSource[indexPath] {
// 첫번째 section의
case .section1Item(_):
print("supplementrayView.section1Item")
switch kind {
// header view
case UICollectionView.elementKindSectionHeader:
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Section1HeaderView.ID, for: indexPath) as! Section1HeaderView
view.configure(title: "section1")
return view
// footer view
case UICollectionView.elementKindSectionFooter:
break
default:
break
}
case .section2Item(_):
print("supplementrayView.section2Item")
switch kind {
case UICollectionView.elementKindSectionHeader:
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Section1HeaderView.ID, for: indexPath) as! Section1HeaderView
view.configure(title: "section2")
return view
case UICollectionView.elementKindSectionFooter:
break
default:
break
}
}
// 만약 위 switch 중 break으로 떨어지면 UICollectionReusableView를 리턴해줘야 함.
// 근데 UICollectionReusableView()를 리턴하면 안됨. dequeueReusableSupplementaryView를 이용해서 실체가 있는 view를 만들고 크기를 0으로 줘서 리턴하는 방식을 이용했다.
let dummyView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Section1HeaderView.ID, for: indexPath) as! Section1HeaderView
dummyView.frame.size.width = 0.0
dummyView.frame.size.height = 0.0
return dummyView
}
// dataSource 적용 후 리턴
let dataSource = MySectionDataSource.init(
configureCell: configureCell,
configureSupplementaryView: supplementrayView
)
return dataSource
}
collectionView item 구현
vm.sections
// 바로 위에서 구현했던 dataSource를 파라미터로 넘겨준다.
.bind(to: collectionView.rx.items(dataSource: ViewController.dataSource))
.disposed(by: disposeBag)