UICollectionView CompositionalLayout
공식문서를 열었는데 샘플코드가 14.0 이상으로 나옴. - 글 제일 하단에 참고 부분에 있음
취업 준비하면서 13.0을 기준으로 공부하고 있어서
Xcode를 통해 열어보니까 다행히도 13.0 이상에서도 사용이 가능했음
이전에는 DataSource만 SnapShot을 사용하고 레이아웃은 FlowLayout을 사용했었는데, 이 부분마저도 공부해보려고 함.
✅ SnapShot + FlowLayout 포스팅
2022.09.04 - [iOS] - [iOS] UICollectionView에 대해서 알아보기 7편 (UICollectionViewDiffableDataSource)
목차
1. UI결과물
2. 레이아웃 코드
3. 레이아웃 이론 학습
4. 전체코드
✅ UI 결과물
✅ 레이아웃 코드 ✅
func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { section, env -> NSCollectionLayoutSection? in
let section = MySection(rawValue: section)
switch section {
case .animation:
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalWidth(0.25)
)
)
item.contentInsets = .init(top: 0, leading: 5, bottom: 0, trailing: 5)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25)
),
subitems: [item]
)
group.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
section.orthogonalScrollingBehavior = .continuous
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0)
let minScale: CGFloat = 0.7
let maxScale: CGFloat = 1.1
let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width), minScale)
item.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return section
case .category:
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalWidth(0.25)
)
)
item.contentInsets = .init(top: 0, leading: 5, bottom: 0, trailing: 5)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25)
),
subitems: [item]
)
group.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
return section
case .custom:
let inset: CGFloat = 2.5
// Items
let largeItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
let largeItem = NSCollectionLayoutItem(layoutSize: largeItemSize)
largeItem.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
let smallItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
smallItem.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
// Nested Group
let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25), heightDimension: .fractionalHeight(1))
let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [smallItem])
// Outer Group
let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.5))
let outerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: outerGroupSize, subitems: [largeItem, nestedGroup, nestedGroup])
// Section
let section = NSCollectionLayoutSection(group: outerGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
// Supplementary Item
// let headerItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
// let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerItemSize, elementKind: "header", alignment: .top)
// section.boundarySupplementaryItems = [headerItem]
return section
default:
return nil
}
}
}
✅ 레이아웃 이론
애니메이션의 경우에는 글 하단의 참고 링크를 확인할 것.
애니메이션은 엄청 어렵지는 않으나, 수학적 사고가 필요한 것 같음.
추가로 orthogonalScrollingBehavior를 사용하여 페이징을 처리할 수 있음
✅ 전체 코드 ✅
//
// MuseumCollectionViewController.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/09/18.
//
import UIKit
import SnapKit
import Nuke
import RxSwift
final class MuseumCollectionViewController: UIViewController {
var disposeBag = DisposeBag()
var businesses = KmongAsset.비즈니스.toArray()
enum MySection: Int {
case animation
case category
case custom
}
enum Item: Hashable {
case animationItem(UIImage)
case categoryItem(UIImage)
case customItem(UIImage)
}
var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
var dataSource: UICollectionViewDiffableDataSource<MySection, Item>!
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
fetchDataSource()
fetchSnapshot()
bind()
}
func bind() {
collectionView.rx.itemSelected
.debug("✅")
.bind { _ in }
.disposed(by: disposeBag)
}
func setupCollectionView() {
collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
view.addSubview(collectionView)
collectionView.register(MuseumCategoryCVC.self, forCellWithReuseIdentifier: MuseumCategoryCVC.id)
collectionView.snp.makeConstraints {
$0.edges.equalTo(view.safeAreaLayoutGuide)
}
}
func fetchDataSource() {
dataSource = UICollectionViewDiffableDataSource<MySection, Item>(
collectionView: collectionView) {
collectionView, indexPath, itemIdentifier in
let section = MySection(rawValue: indexPath.section)
print("section: \(section): indexPath \(indexPath): item \(itemIdentifier) \(type(of: itemIdentifier))")
var value: Any
switch itemIdentifier {
case .animationItem(let item):
value = item
case .categoryItem(let item):
value = item
case .customItem(let item):
value = item
}
switch section {
case .animation:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: MuseumCategoryCVC.id,
for: indexPath
) as? MuseumCategoryCVC else { return UICollectionViewCell() }
cell.configureCell(with: value as! UIImage)
return cell
case .category:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: MuseumCategoryCVC.id,
for: indexPath
) as? MuseumCategoryCVC else { return UICollectionViewCell() }
cell.configureCell(with: value as! UIImage)
return cell
case .custom:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: MuseumCategoryCVC.id,
for: indexPath
) as? MuseumCategoryCVC else { return UICollectionViewCell() }
cell.configureCell(with: value as! UIImage)
return cell
default:
return UICollectionViewCell()
}
}
}
func fetchSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<MySection, Item>()
snapshot.appendSections([.animation, .category, .custom])
businesses.map { snapshot.appendItems([.animationItem($0)], toSection: .animation) }
businesses.map { snapshot.appendItems([.categoryItem($0)], toSection: .category) }
businesses.map { snapshot.appendItems([.customItem($0)], toSection: .custom) }
dataSource.apply(snapshot, animatingDifferences: true)
}
func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { section, env -> NSCollectionLayoutSection? in
let section = MySection(rawValue: section)
switch section {
case .animation:
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalWidth(0.25)
)
)
item.contentInsets = .init(top: 0, leading: 5, bottom: 0, trailing: 5)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25)
),
subitems: [item]
)
group.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
section.orthogonalScrollingBehavior = .continuous
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0)
let minScale: CGFloat = 0.7
let maxScale: CGFloat = 1.1
let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width), minScale)
item.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return section
case .category:
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalWidth(0.25)
)
)
item.contentInsets = .init(top: 0, leading: 5, bottom: 0, trailing: 5)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalWidth(0.25)
),
subitems: [item]
)
group.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
return section
case .custom:
let inset: CGFloat = 2.5
// Items
let largeItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
let largeItem = NSCollectionLayoutItem(layoutSize: largeItemSize)
largeItem.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
let smallItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
smallItem.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
// Nested Group
let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25), heightDimension: .fractionalHeight(1))
let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [smallItem])
// Outer Group
let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.5))
let outerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: outerGroupSize, subitems: [largeItem, nestedGroup, nestedGroup])
// Section
let section = NSCollectionLayoutSection(group: outerGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
// Supplementary Item
// let headerItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
// let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerItemSize, elementKind: "header", alignment: .top)
// section.boundarySupplementaryItems = [headerItem]
return section
default:
return nil
}
}
}
}
class MuseumCategoryCVC: UICollectionViewCell {
static let id: String = "MuseumCategoryCVC"
let categoryImage = UIImageView()
// MARK: - Initialize
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(categoryImage)
categoryImage.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(with item: UIImage) {
categoryImage.nuke_display(image: item)
}
}
class MuseumCategoryHeaderView: UICollectionReusableView {
}
✅ NOTE: - 다이나믹 헤잇!
(아래 공식 문서를 참고하여 작성)
(그냥 여기 다 있음 - 문서는 이게 최고)
https://lickability.com/blog/getting-started-with-uicollectionviewcompositionallayout/
(애니메이션)
https://ios-development.tistory.com/948
'apple > iOS, UIKit, Documentation' 카테고리의 다른 글
Swift HTML 코드 로드하기 (0) | 2022.10.06 |
---|---|
Moya Unable to parse empty data 대응하기 (0) | 2022.09.29 |
[iOS] DiffableDataSource n-Section n-Item (섹션마다 다른 셀) (0) | 2022.09.18 |
Realm 간단하게 구조 적용하기 (0) | 2022.09.06 |
[Realm] Realm migration (Swift) (0) | 2022.09.05 |