안녕하세요.
이번 포스트에서 컬랙션 뷰 flow layout으로 구현했던 기존 여행 앱 피드 화면을 compositional layout으로 리빌딩한 경험을 공유하려고 합니다: ]
리빌딩을 하게된 이유
왼쪽 동영상은 기존에 구현했던 화면입니다.
그리고 디자이너분이 새롭게 오른쪽 동영상처럼 더 이쁜 디자인을 만들어 주셨습니다. 피드 홈에서는 사용자의 여행 후기를 보여주는 포스트 cell로 이루어진 컬랙션 뷰로 위 화면이 구성되어있는데 기존 구현은 컬랙션 뷰의 flow layout을 이용했습니다.
TableView vs CollectionView 선택 과정..
피드의 cell 구성입니다. 컬랙션 뷰를 선택한 이유는 cell의 높이가 서로 다르기 때문입니다. (초기에는 thumbnail 영역 뷰 높이 또한 이미지 개수 여부에 따라 달랐습니다.) 서로 다른 데이터에 따라 dynamic한 cell의 높이를 제공하고자 UICollectionViewDelegateFlowLayout 프로토콜을 활용해서 동적으로 cell 높이를 계산하는 방법이 괜찮겠다고 생각했습니다. 물론 테이블 뷰 셀 또한 쉽게 self-resizing이 가능합니다. (cell's self-resizing관련 개념 활용 포스트 정리 링크)
물론 이 화면의 경우는 위에서 아래로 스크롤되기에, 테이블 뷰와 tableView.estimatedRowHeight 속성을 이용해서 테이블 뷰가 자동으로 런타임시 계산하는 선택지도 있었습니다. 그러나 위에 언급한 flow layout의 델리게이트를 활용해 더 정확하게 계산해주고 싶었기 때문에 선택했었습니다.
CollectionView Flow layout vs Compositional layout
새롭게 개선된 화면에서는 기존에 없던 cell 영역에 shadow가 추가 됬고 화면의 맨 위, 맨 아래에 cornerRadius가 추가됬습니다. 그래서 기존에 구현한 컬랙션 뷰 자체에 Inset과 radius, shadow를 주면 빠르게 새롭게 디자인된 화면을 구성할 수 있었습니다.
팀원이 구현한 이 화면의 일부에서도 사용되는데, 컴포지셔널로 구현했기 때문에,,, 저도 피드의 cell 영역을 플로우 레이아웃에서 컴포지셔널 레이아웃으로 전환하기로 결정했습니다.
Compositional layout의 장점 및 느낀점
- 위 사진처럼 item, group, section의 조화를 통해 가로 세로 스크롤 여부에 유연하게 대응할 수 있습니다. (view depth 단축)
- tableView 처럼 estimated를 활용해 item의 크기를 추정하고 런타임시 동적으로 item 크기를 계산해 화면에 보여줍니다.
- decorationView를 통해 section의 view를 커스텀할 수 있습니다.
저는 flow layout delegate 메서드를 활용해 데이터에 따라 동적으로 텍스트 높이를 계산해 cell에 적용했던 코드를 제거할 수 있었습니다.
private var postSection: NSCollectionLayoutSection {
typealias LayoutSize = NSCollectionLayoutSize
typealias Const = Constants.Layout
let groupHeight = Const.estimatedCellHeight // 289
let itemSize = LayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(groupHeight))
let groupSize = LayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(groupHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
// decorationView 추가
let whiteRoundView = NSCollectionLayoutDecorationItem.background(elementKind: InnerRoundRectReusableView.id)
section.contentInsets = Const.groupInset
whiteRoundView.contentInsets = Const.groupInset
section.decorationItems = [whiteRoundView]
return section
}
item의 크기, group의 크기를 선언합니다. 그 후 item을 기반으로하는 group, group기반의 section을 설정하면 됩니다. 여기서 NSCollectionLayoutSize를 통해서 item size, group size를 지정할 수 있는데, estimated가 좋긴 합니다... 기존의 cell 높이 동적 계산을 구체적으로 하지 않아도 되기 때문입니다.
여기서 itemSize height를 동적으로 추정해도 groupSize의 height를 명확하게 지정하면 실질적으로 반영이 안될 수 있습니다. 주의해야 합니다.
func makePostLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, _) -> NSCollectionLayoutSection? in
guard sectionIndex == (self?.sectionIndex ?? 0) else { return nil }
return self?.postSection
}.set {
// 컴포지셔널 레이아웃의 인스턴스에 decoration view를 등록합니다.
$0.register(
InnerRoundRectReusableView.self,
forDecorationViewOfKind: InnerRoundRectReusableView.id)
}
}
section을 기반으로 UICollectionViewCompostiionalLayout을 생성 후 collectionViewLayout에 대입하면 됩니다.
컴포지셔널을 사용하면서 장점은 한 개의 컬랙션 뷰에 서로 다른 scroll 방향과 크기를 가진 section을 통해 화면을 유연하게 구현할 수 있다는 장점이었습니다.
section의 decorationItems도 많이 대박인 것 같습니다... section의 외관을 커스텀할 수 있다니..