[UIKit] Modern Collection View: Compositional Layout

Junyoung Park·2022년 12월 6일
0

UIKit

목록 보기
115/142
post-thumbnail

Modern Collection View [5] - Compositional Layout | Per Section Composable Layout

Modern Collection View: Compositional Layout

구현 목표

  • 섹션 별 서로 다른 구성을 가진 컬렉션 뷰 구현

구현 태스크

  • 카로셀 컬렉션 뷰 구현
  • 그룹 페이징 컬렉션 뷰 구현
  • 리스트 컬렉션 뷰 구현
  • 회전 방향에 따른 반응형 그리드 컬렉션 뷰 구현

핵심 코드

let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
  • 주어진 데이터 소스의 스냅샷에서 특정 인덱스에 해당하는 섹션을 꺼내온 뒤, 해당 섹션의 종류에 따른 구성을 컴포지셔널 레이아웃으로 만들어 내보내기
case .storiesCarousel:
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(1)))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .absolute(70), heightDimension: .estimated(1)), subitems: [item])
                let section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 16
                section.contentInsets = .init(top: 0, leading: 16, bottom: 16, trailing: 16)
                section.orthogonalScrollingBehavior = .continuous
                section.supplementariesFollowContentInsets = false
                section.boundarySupplementaryItems = [self.supplementaryHeaderItem(), self.supplementarySeparatorFooterItem()]
                return section
  • 인스타그램 스토리와 같은 카로셀
  • NSCollectionLayoutGroup.horizontal을 통해 수직이 아닌 수평 방향으로 이어지는 컬렉션 뷰 방향
  • 각 패딩과 헤더, 푸터 등을 설정
  • continuous는 셀 중간이 잘려도 연속적으로 이어지는 페이징 기법
case .bannerCarousel:
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(1)))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.77), heightDimension: .estimated(1)), subitems: [item])
                let section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 16
                section.supplementariesFollowContentInsets = false
                section.contentInsets = .init(top: 0, leading: 16, bottom: 16, trailing: 16)
                section.boundarySupplementaryItems = [self.supplementaryHeaderItem(), self.supplementarySeparatorFooterItem()]
                section.orthogonalScrollingBehavior = .continuous
                return section
  • 배너 카드 카로셀 구성
  • 스토리와는 서로 다른 레이아웃 사이즈가 핵심
case .columnCarousel:
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(1))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
                let containerGroup = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .fractionalWidth(0.8), heightDimension: .estimated(1)), subitems: Array(repeating: group, count: 3))
                let section = NSCollectionLayoutSection(group: containerGroup)
                section.contentInsets = .init(top: 0, leading: 16, bottom: 16, trailing: 16)
                section.interGroupSpacing = 16
                section.supplementariesFollowContentInsets = false
                section.boundarySupplementaryItems = [self.supplementaryHeaderItem(), self.supplementarySeparatorFooterItem()]
                section.orthogonalScrollingBehavior = .groupPaging
                return section
  • 컨테이너 그룹 NSCollectionLayoutGroup이 핵심
  • 레이아웃 그룹을 특정 개수만큼 연속해서 쌓아놓은 게 하나의 컨테이너 그룹으로 해당 그룹을 수평 방향으로 이어붙인 구성
  • .groupPaging 기법은 .continuous처럼 셀이 잘려도 이어지는 게 아니라 셀 단위로 넘길 수 있음
case .list:
                var listConfiguration = UICollectionLayoutListConfiguration(appearance: .grouped)
                listConfiguration.headerMode = .supplementary
                listConfiguration.footerMode = .supplementary
                return NSCollectionLayoutSection.list(using: listConfiguration, layoutEnvironment: layoutEnvironment)
  • 컬렉션 뷰가 제공하는 디폴트 리스트 구성을 사용
case .grid:
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(1)))
                guard let orientation = UIApplication.shared.windows.first(where: { $0.isKeyWindow})?.windowScene?.interfaceOrientation else { return nil }
                let noOfColumns: Int
                switch orientation {
                case .landscapeLeft, .landscapeRight :
                    noOfColumns = 8
                default: noOfColumns = 4
                    
                }
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(1)), subitem: item, count: noOfColumns)
                group.interItemSpacing = .fixed(16)
                let section = NSCollectionLayoutSection(group: group)
                section.interGroupSpacing = 16
                section.contentInsets = .init(top: 0, leading: 16, bottom: 16, trailing: 16)
                section.boundarySupplementaryItems = [self.supplementaryHeaderItem(), self.supplementarySeparatorFooterItem()]
                return section
  • 현재 디바이스의 회전 방향이 가로/세로 모드인지 확인한 뒤 칼럼 개수를 동적으로 구성하는 그리드 레이아웃
  • 특정 시뮬레이어 환경(iPhone 14)에서는 traitCollection의 프로퍼티를 통해 가로/세로 모드를 확인하는 게 정확하지 않았기 때문에 UIWindow를 통해 직접적으로 확인
let noOfColumns = layoutEnvironment.traitCollection.horizontalSizeClass == .compact ? 4 : 8
  • 위 코드를 통한 가로/세로 모드 확인은 iPhone 12에서는 정상적으로 작동하는 것을 확인했지만, 일단 모든 시뮬레이터 환경에서 동작하는 UIWindow를 통한 코드를 선택

구현 화면

'단 하나'의 컬렉션 뷰를 통해 위와 같은 뷰를 그릴 수 있다는 건 컴포지녀설 레이아웃이 가진 어마어마한 힘을 단적으로 보여준다고 해도 과언이 아니다! 여러 개의 컬렉션 뷰를 사용한다면 특정 구성이 바뀔 때마다 구성을 바꿔야 하고, 별도의 뷰를 로드해야 하는 등 불필요한 리소스 사용이 불가피할 것이기 때문이다!

profile
JUST DO IT

0개의 댓글