[TIL #18] 2023 / 04 / 28
오늘은 CompositionalLayout에서 Cell의 크기를 동적으로 조절해 보려고 합니다.
기본 사용방법은 [Swift/TIL #13] CollectionView의 CompositionalLayout 를 참고하세요.
시작
이와 같이 더 보기 버튼이 있는 컬렉션뷰를 만들어 보려고 합니다.
일단 레이블의 numberOfLines는 3줄로 제한을 걸어두고,
더 보기를 버튼을 클릭하면 나머지 글 전부를 볼 수 있게 만들 예정입니다.
먼저 셀 코드를 확인해 봅시다.
레이블과 버튼이 있는 Cell을 만들어 줬습니다.
그리고 버튼이 동작 시 델리게이트로 넘겨줘서 Label을 바꿔줄 예정입니다.
protocol DynamicCustomCellDelegate: AnyObject {
func showHideButtonTapped(_ cell: DynamicCustomCell, sender: UIButton)
}
class DynamicCustomCell: UICollectionViewCell {
static let Identifier = "DynamicCustomCell"
weak var delegate: DynamicCustomCellDelegate?
let titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 3
label.textAlignment = .center
return label
}()
lazy var showHideButton: UIButton = {
let button = UIButton()
button.setTitle("더보기", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.addTarget(self, action: #selector(showHideButtonHandler), for: .touchUpInside)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 10
self.clipsToBounds = true
self.contentView.backgroundColor = .lightGray
self.contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints {
$0.top.leading.trailing.bottom.equalToSuperview()
}
self.contentView.addSubview(showHideButton)
showHideButton.snp.makeConstraints {
$0.centerY.equalTo(titleLabel)
$0.trailing.equalToSuperview().offset(-30)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func showHideButtonHandler(_ sender: UIButton) {
delegate?.showHideButtonTapped(self, sender: sender)
}
}
이제 UICollectionViewCompositionalLayout로
컬렉션뷰 레이아웃을 만들어 봅시다.
layoutSize를 설정할 때, 최종크기가 렌더링 시 정해지는 estimated를 사용해줘야 합니다.
estimated에 대한 설명은 [Swift/TIL #13] CollectionView의 CompositionalLayout 를 참고해 주세요.
코드는 이렇습니다. 여기선 단일 섹션으로 간단히 만들어 줬습니다.
private func createLayout() -> UICollectionViewLayout {
// 👉 heightDimension - estimated
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(100))
let itme = NSCollectionLayoutItem(layoutSize: itemSize)
// estimated를 사용하게 되면 contentInsets 값은 무시가 됩니다.
itme.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: .fixed(20), trailing: nil, bottom: nil)
// 👉 heightDimension - estimated
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [itme])
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
여기서 알고 가야 할 포인트가 3가지 있습니다.
1. estimated는 렌더링 시 크기가 정해지므로 아무 값이나 넣어줘도 무방합니다.
다만, 실제 크기와 설정한 크기의 값이 적을수록 퍼포먼스가 좋아진다고 하네요.
2. 동적으로 크기를 변화시키려면, itemSize와 groupSize 두 쪽 모두 estimated로 설정해줘야 합니다.
왜냐하면, group 안쪽에 item이 위치해 있죠? 그러므로 한쪽만 사용하게 되면 아마도 엄청난 계산이 필요할 거예요...
위 코드에서는 높이(height)가 동적으로 변하기 때문에 heightDimension에 대한 설정 모두 estimated로 적용함.
3. estimated를 사용 시, 여백을 설정하는 contentInsets 값은 무시가 됩니다.
그러므로 inset 또는 공간을 만들어줘야 할 경우, edgeSpacing를 사용하여 여백을 만들어 줘야 합니다.
여기서는 간단히 사용하기 위해 UICollectionViewDiffableDataSource 대신
dataSource를 채택하여 컬렉션 뷰를 만들어 줬습니다.
extension DynamicViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DynamicCustomCell.Identifier, for: indexPath) as? DynamicCustomCell else {
fatalError("Faild to load CustomCell")
}
cell.delegate = self
let text = "엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n엄청 긴 글 \n마지막"
cell.titleLabel.text = text
return cell
}
}
이제 기본적인 컬렉션뷰 만들기는 끝났습니다.
만들어 둔 버튼에 로직을 추가하여 기능을 완성해 봅시다.
위에서 Cell의 크기를 동적으로 지정해 줬기 때문에 titleLabel의 줄 수만 조정 후, 컬렉션 뷰를 업데이트시켜 주면,
레이블에 길이에 따라 늘었다 줄었다 하는 더 보기 버튼 완성입니다.
extension DynamicViewController: DynamicCustomCellDelegate {
func showHideButtonTapped(_ cell: DynamicCustomCell, sender: UIButton) {
switch sender.currentTitle {
case "더보기":
sender.setTitle("숨기기", for: .normal)
cell.titleLabel.numberOfLines = 0
DispatchQueue.main.async {
self.collectionView.reloadData()
}
case "숨기기":
sender.setTitle("더보기", for: .normal)
cell.titleLabel.numberOfLines = 3
DispatchQueue.main.async {
self.collectionView.reloadData()
}
default: break
}
}
}
전체 코드는 GitHub 를 참고하세요.
문제
여러 개의 Cell이 존재하면, 선택한 버튼의 Cell이 아니라 랜덤으로 아무 Cell의 크기가 변하더라고요.
이유는 아마도 컬렉션 뷰의 reloadData에서 문제가 발생한 것으로 보입니다.
간략히 문제를 정리해보면
1. Button이 위치한 Cell의 인덱스를 특정할 수 없다.
2. reloadData로 전체 셀을 다시 로드해서 순서가 변하게 된다.
이것들이 문제 같습니다.
그래서 조금 찾아보니 reloadData 말고도 셀을 업데이트하는 메서드가 3가지 더 있더라고요.
참고로 UICollectionViewDiffableDataSource에서도 마찬가지로 아래 메서드들이 존재합니다. (사용 가능)
func reloadSections(_ sections: IndexSet)
func reloadItems(at indexPaths: [IndexPath])
func reconfigureItems(at indexPaths: [IndexPath])
여기서 먼저 reloadItems를 사용해 봤는데, 이상하게도 버튼을 2번씩 눌러줘야만 동작이 되더라고요.
이건 이유를 도저히 모르겠네요...
그래서 iOS 15부터 사용가능한 reconfigureItems를 사용해 각 아이템을 업데이트 해봤습니다.
reconfigureItems는 새로운 데이터로 셀을 다시 구성할 때 사용한다고 합니다.
새롭게 뷰를 리로드하는게 아니라서 reloadItems보다 성능면에서 좋다고 하네요.
그리고 estimated 같이 자동으로 셀의 크기를 조절하게 되면, 먼저 셀을 재구성한 뒤 셀의 크기를 조절해 준다고 합니다.
extension DynamicViewController: DynamicCustomCellDelegate {
func showHideButtonTapped(_ cell: DynamicCustomCell, sender: UIButton) {
// 각 셀의 index를 알수 있게 불러와 줍시다.
guard let indexPath = collectionView.indexPath(for: cell) else { return }
switch sender.currentTitle {
case "더보기":
sender.setTitle("숨기기", for: .normal)
cell.titleLabel.numberOfLines = 0
DispatchQueue.main.async {
self.collectionView.reconfigureItems(at: [indexPath])
}
case "숨기기":
sender.setTitle("더보기", for: .normal)
cell.titleLabel.numberOfLines = 3
DispatchQueue.main.async {
self.collectionView.reconfigureItems(at: [indexPath])
}
default: break
}
}
}
완성
전체 코드는 GitHub 를 참고하세요.
'프로젝트' 카테고리의 다른 글
[Swift/TIL #20] 이미지 메모리 최적화 방법들 (WWDC 18) (0) | 2023.05.02 |
---|---|
[Swift/TIL #19] 현재 입력된 텍스트의 줄 수 구하기 (0) | 2023.04.30 |
[Swift/TIL #17] viewDidLayoutSubviews 간단 정리 (0) | 2023.04.19 |
[Swift/TIL #16] Layer 그림자 설정 중, 보라색 경고 (dynamic shadows) (0) | 2023.04.18 |
[Swift/TIL #15] UIMenu를 사용해보자 (0) | 2023.04.17 |