UICollectionView From Scratch [2] - Flow Layout | Self-Sizing Cells | Data Source
systemLayoutSizeFitting
적용override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
let padding: CGFloat = 8
let noOfItems = traitCollection.horizontalSizeClass == .compact ? 4 : 8
let itemWidth = (UIScreen.main.bounds.width - (padding * 2)) / CGFloat(noOfItems)
return super.systemLayoutSizeFitting(.init(width: itemWidth, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
self-sizing
의 핵심, 현재 사이즈에 따라 주어진 크기 변경UIView.layoutFittingExpandedSize.height
를 통해 동적으로 얻어내기label.adjustsFontForContentSizeCategory = true
let imageHeightConstraint = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor)
imageHeightConstraint.priority = .init(999)
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
estimatedItemSize
를 통해 자동 사이즈로 리사이징 가능하도록 설정import UIKit
import Combine
class SingleSectionCharactersViewController: UIViewController {
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.sectionInset = .init(top: 0, left: 8, bottom: 0, right: 8)
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(CharacterCell.self, forCellWithReuseIdentifier: CharacterCell.identifier)
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.identifier)
return collectionView
}()
private lazy var segmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: Universe.allCases.map({ $0.title }))
control.selectedSegmentIndex = 0
control.addTarget(self, action: #selector(didTapControl), for: .valueChanged)
return control
}()
private let input: PassthroughSubject<SingleSectionCharactersViewModel.Input, Never> = .init()
private var cancellables = Set<AnyCancellable>()
private let viewModel = SingleSectionCharactersViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
collectionView.collectionViewLayout.invalidateLayout()
}
private func setUI() {
view.addSubview(collectionView)
view.backgroundColor = .systemBackground
navigationItem.titleView = segmentedControl
}
private func bind() {
viewModel.transform(input: input.eraseToAnyPublisher())
viewModel
.univsere
.sink { [weak self] _ in
self?.collectionView.reloadData()
}
.store(in: &cancellables)
}
@objc private func didTapControl() {
input.send(.controlDidTap(segmentedControl.selectedUniverse))
}
}
traitCollectionDidChange
를 오버라이드, 유저 설정한 액세서빌리티가 변경될 때 자동으로 컬렉션 뷰 레이아웃을 invalidate
하기extension SingleSectionCharactersViewController: UICollectionViewDelegate {
}
extension SingleSectionCharactersViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.univsere.value.stubs.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CharacterCell.identifier, for: indexPath) as? CharacterCell else { fatalError() }
let model = viewModel.univsere.value.stubs[indexPath.row]
cell.configure(with: model)
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.identifier, for: indexPath) as? HeaderView else { fatalError() }
header.configure(with: viewModel.univsere.value.title)
return header
}
}
extension SingleSectionCharactersViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let header = HeaderView()
header.configure(with: viewModel.univsere.value.title)
return header.systemLayoutSizeFitting(.init(width: collectionView.bounds.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
}
systemLayoutSizeFitting
을 헤더 뷰를 리턴할 때 적용하기 위해 referenceSizeForHeaderInSection
함수에서 해당 헤더 뷰의 사이즈를 그때 계산해서 리턴import UIKit
import Combine
class SingleSectionCharactersViewModel {
enum Input {
case controlDidTap(Universe)
}
let univsere: CurrentValueSubject<Universe, Never> = .init(.ff7r)
private var cancellables = Set<AnyCancellable>()
func transform(input: AnyPublisher<Input, Never>) {
input
.sink { [weak self] result in
switch result {
case .controlDidTap(let control): self?.univsere.send(control)
}
}
.store(in: &cancellables)
}
}