[iOS] UICollectionView CompositionalLayout
UICollectionView CompositionalLayout
공식문서를 열었는데 샘플코드가 14.0 이상으로 나옴. - 글 제일 하단에 참고 부분에 있음
취업 준비하면서 13.0을 기준으로 공부하고 있어서
Xcode를 통해 열어보니까 다행히도 13.0 이상에서도 사용이 가능했음
이전에는 DataSource만 SnapShot을 사용하고 레이아웃은 FlowLayout을 사용했었는데, 이 부분마저도 공부해보려고 함.
✅ SnapShot + FlowLayout 포스팅
2022.09.04 - [iOS] - [iOS] UICollectionView에 대해서 알아보기 7편 (UICollectionViewDiffableDataSource)
[iOS] UICollectionView에 대해서 알아보기 7편 (UICollectionViewDiffableDataSource)
UICollectionView에 대해서 알아보기 7편 (UICollectionViewDiffableDataSource) iOS 13이상에서 사용하능하다. 결과 코드 7편에서는 이거 알아보자! UICollectionViewDiffableDataSource OverView 음,,..
rldd.tistory.com
목차
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: - 다이나믹 헤잇!
Dynamic cell height in UICollectionViewCompositionalLayout
Continuing my UICollectionViewCompositionalLayout series from last time. Today we will discuss how to make cell hight dynamic!
medium.com
(아래 공식 문서를 참고하여 작성)
Apple Developer Documentation
developer.apple.com
(그냥 여기 다 있음 - 문서는 이게 최고)
https://lickability.com/blog/getting-started-with-uicollectionviewcompositionallayout/
Getting Started with UICollectionViewCompositionalLayout
At WWDC 2019, Apple introduced and later documented an unbelievable API for building complex layouts with ease. UICollectionViewCompositionalLayout promised to simplify collection view layouts using a more declarative approach without the need to subclass
lickability.com
(애니메이션)
https://ios-development.tistory.com/948
[iOS - swift] 4. UICollectionViewCompositionalLayout - 개념 (orthogonalScrollingBehavior, 수평 스크롤, visibleItemsInval
1. UICollectionViewCompositionalLayout - 개념 (section, group, item) 2. UICollectionViewCompositionalLayout - 개념 SupplementaryView, Header, Footer) 3. UICollectionViewCompositionalLayout - 개념..
ios-development.tistory.com