기존 UICollectionView를 씀에 있어서 보통 레이아웃 잡을땐 FlowLayout을 주로 써왔고 Compositional Layout의 존재는 솔직히 몰랐는데 이번 플젝을 진행하면서 암것도 모르고 컬렉션뷰 세개를 붙히려 하던 내역이 담긴 PR을 보던 유스승님과 이스승님께서 가르침을 주셨다.
FlowLayout으로 여러개 말고 Compositional Layout을 써보는게 어떨까요? (그걸 들은 나... 그게 뭐예요..?)
이번 플젝에선 키오스크 앱이 컨셉이라 첫번째 섹션과 두번째 섹션은 수평, 세번째 섹션은 수직으로 축을 가집니다
또 세 섹션 모두 셀의 레이아웃이 많이 다르다. 1행의 형태를 가지는 첫번째 섹션 그리고 2행의 형태를 가지는 두번째 섹션, 1열의 형태를 가지는 세번째 섹션. 각자 방향도 다르고 레이아웃도 많이 다르며 내부적으로 나름 헤더와 푸터도 가지는 섹션이 있어요.
이렇게 복잡한 레이아웃을 처리하는 데에 유용하게 쓰이도록 나온 레이아웃이 이 Compositional Layout
아하..! 그럼 FlowLayout이랑 Compositional Layout이랑 뭐가 다른데?
FlowLayout은 일단 iOS 6부터 도입됐고 말그대로 '흐르듯이' 배치되는 것이 특징이고 Compositional은 iOS 13에서 도입됐으며 더 복잡하고 다양한 레이아웃을 쉽게 구현할 수 있게 해준다. 오... 근데 솔직히 여기까지도 아직 Flow Layout 쓰면 되는거 아님? 이 생각했는데
아래 코드를 보자 (이거말고도 이유는 더 많겠지만!)
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// 인덱스에 따라 다른 크기 반환
if indexPath.item % 3 == 0 {
return CGSize(width: 100, height: 100)
} else if indexPath.item % 3 == 1 {
return CGSize(width: 100, height: 150)
} else {
return CGSize(width: 150, height: 100)
}
}
기존 FlowLayout에서 item 사이즈 설정할 땐 웬만해선 수치로 주고 비율로 주려면 다른 방법을 써야 했음. (혹시나 아니면 댓글로 알려주세요!)
근데 Compositional에서의 방식은
// 다양한 크기의 아이템 정의
let leadingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.7),
heightDimension: .fractionalHeight(1.0)
)
)
let trailingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)
)
)
// 작은 아이템들을 담는 그룹
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.3),
heightDimension: .fractionalHeight(1.0)
),
subitems: [trailingItem, trailingItem]
)
// 큰 아이템과 작은 아이템들의 그룹을 포함하는 메인 그룹
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.5)
),
subitems: [leadingItem, trailingGroup]
)
이게 코드양이 어찌보면 많아져서 굳이 할 수 있는걸 복잡하게 한다 생각할 수도 있는데..(아님 말고)
안에서 보면 widthDimension, heightDimension을 각 층마다 주었음. 근데 여기 안엔 FlowLayout과 다르게 세 가지 접근 방식이 있음
절대값(.absolute), 상태값(.fractional), 추정값(.estimated)
절대값은 말그대로 수치를 넣어주는 방식. 기존 FlowLayout 코드에서 수치를 넣던 방식과 가장 유사함.
상태값은 상위에서 잡아둔 영역에서의 일정 비율만큼 자기가 차지하겠다!는 방식. 뭐 item이 group의 하위니깐 item에 fractionalWidth(0.5)가 되어있으면 group에서 잡아둔 width의 절반을 가져간다는 말이지요.
추정값은 얼추 이정도겠다!하고 일단 추정치를 넣어두고 내부 계산을 끝나고 정확한 값을 넣는 느낌. tableView에서의 estimatedHeight에 대해 다룬 적이 있는데 이와 유사한 것 같음. 대신 너무 러프하게 잡아버리면 초기 추정값으로 먼저 구성하고 실제 값으로 재구성되면 갑자기 스크롤 위치가 바뀌어버릴 수도 있다고 하니 고려해두기 !!
쨋든, 위에서 말하는 걸 보면 후자의 방식이 훨씬 유연하고 복잡한 구성도 좀 더 관리가 쉽겠다는 생각이 들잖음? 아닌가요...?
그래서 일단 이 Compositional Layout의 구조는 어떻게 잡는지에 대해 알아보기로 해보아요
NSCollectionLayoutSection
이 CompositionalLayout에서의 핵심 구성 요소 중 하나가 이 NSCollectionLayoutSection이 되시겠습니다
얘는 컬렉션 뷰의 섹션 레이아웃을 정의하는 객체고 계층적 레이아웃 구조를 가짐
- Item -> Group -> Section -> Layout
좌측에서 우측으로 갈 수록 더 상위 개념이라고 보면 됨
섹션은 그룹들의 컨테이너, 보통 우리가 테이블뷰에서 했던 것처럼 하나의 단위를 형성함
또 위에서 화살표를 보면 알 수 있듯 그룹들은? 아이템들의 컨테이너임.
아이템, 그룹, 섹션마다 레이아웃 설정들을 해줄 수 있고 각 하위 개념들은 이들을 포함하는 상위 개념에 영향을 받음(이건 뒤에서 다룰게용)
자 그래서 이번 프로젝트를 예시로 들어보자면 !
이 와이어프레임을 기준으로 컬렉션뷰로 표현할만한 컨텐츠가 뭐가 있느냐 하면 상단부터 카테고리, 버거 품목, 주문 내역 정도 있겠죠?
이중에 버거 품목을 기준으로 한번 보도록 할게요
일단 이 2행 2열짜리 셀을 보여줘야 함. 이에 대한 코드를 먼저 보자면
static func createMenuItemSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(
top: 12,
leading: 12,
bottom: 12,
trailing: 12
)
let horizontalGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)
)
let horizontalGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: horizontalGroupSize,
repeatingSubitem: item,
count: 2
)
let verticalGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(400)
)
let verticalGroup = NSCollectionLayoutGroup.vertical(
layoutSize: verticalGroupSize,
repeatingSubitem: horizontalGroup,
count: 2
)
verticalGroup.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 12,
bottom: 0,
trailing: 12
)
let section = NSCollectionLayoutSection(group: verticalGroup)
section.orthogonalScrollingBehavior = .groupPaging
let footerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(60)
)
let footer = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: footerSize,
elementKind: UICollectionView.elementKindSectionFooter,
alignment: .bottom
)
section.boundarySupplementaryItems = [footer]
section.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 0,
bottom: 0,
trailing: 24
)
return section
}
되게 길죠? 저도 첨 공부했을 때 많이 놀랐어요.. 일단 푸터는 논외로 하고 지금 당장은 저 2by2 셀을 띄워야하니 저 부분에 영향 주는 코드만 뜯어서 봅시다
let section = NSCollectionLayoutSection(group: verticalGroup)
section.orthogonalScrollingBehavior = .groupPaging
이거 살짝 역순으로 볼게요~ 아래에서 위로 !
일단 이 섹션에 대해서 설정을 해주고 있음, 섹션 객체를 만들어서 나중에 return 해야 하는데 이를 생성하려면 group이라는걸 파라미터로 받음. 그 말은 즉슨 어떤 그룹으로 이 섹션을 구성할거냐? 정도로 이해하면 될듯
그리고 .groupPaging은 이 섹션에서의 페이징을 그룹 단위로 하겠다! 는 말임. 우린 2by2를 한 그룹으로 여길것이기 때문에 알아두고
let horizontalGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)
)
let horizontalGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: horizontalGroupSize,
repeatingSubitem: item,
count: 2
)
let verticalGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(400)
)
let verticalGroup = NSCollectionLayoutGroup.vertical(
layoutSize: verticalGroupSize,
repeatingSubitem: horizontalGroup,
count: 2
)
verticalGroup.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 12,
bottom: 0,
trailing: 12
)
여기서 보면 verticalGroupSize, horizontalGroupSize가 있음.
아이템과 그룹은 사이즈를 지정해줘야하는데 이걸 NSCollectionLayoutSize 객체를 통해서 지정해주게 되고, verticalGroup은 horizontalGroup을 담고 horizontalGroup은 item을 담는다는 걸 알 수 있음.
근데 여기서 보면 verticalGroup에서 LayoutGroup.vertical을 통해 수직 방향임을 명시하고 count를 2로 해서 두 개의 horizontalGroup을 수직 방향으로 그룹화한다!는 걸 알 수 있고 horizontalGroup은 ~~LayoutGroup.horizontal을 통해 수평 방향, 그리고 내부적으로 item을 두개를 그룹화한다!는걸 알 수 있음.
정리해서 수평 방향으로 아이템 두개를 하나의 horizontalGroup이 가지고, 이 두 그룹들은 verticalGroup이 수직방향으로 가지니깐
2 by 2 형태를 가질 수 있게 된다!
NSCollectionLayoutSize
위에서 얘를 통해 아이템 혹은 그룹의 사이즈를 정했음. 정하는 방식은 위에서 말했던 estimated, absolute, fractional이 있고 이는 베이스가 이를 담아주는 상위 객체가 됨.(예를 들면,, 뭐,, 그룹,,)
그래서 위 코드에선 verticalGroupSize가 높이로 절댓값 400을 가지고 너비는 이를 담는 상위 개념인 컬렉션뷰의 너비를 1.0배만큼 가진다는걸 알 수 있음. 그리고 horizontalGroupSize는 width가 fractionalWidth(1.0)임을 보면 이를 담는 verticalGroupSize의 width와 같은 사이즈를 가지며 height는 이의 0.5배인 절반을 가지게 된다.
결국 같은걸 두줄 가지며 그 같은 줄 하나가 이 상위 개념의 절반의 너비를 가지니 각자 사이즈가 같은 2by2가 만들어지는 것.
뭐 그밖에도 아이템, 그룹, 섹션 별로 각자 contentInset을 줄 수 있으니 활용하면 될 것 같다.
일단 가장 중요하다 생각 되는건 이정도..? 헤더 푸터는 다음에 쓰도록 하겠음
컴포넌트도 무작정 다 가져다가 붙이기보단 어떻게 하는게 이상적일지 계속 근거를 생각해가면서 도출해내야하는데 지금까진 그냥 가져다가 여러개 쓰고 UI를 다 짤수있는줄 알았다. 앞으론 계속 비관적인 시선에서 이 방법 저 방법 모두 생각해보아야 할 필요가 있음,,,
그나저나 이렇게 유연하게 쓸 수 있는건줄 진작에 알았더라면 접근을 다르게 했을 상황이 꽤 많아 아쉽긴한데 지금이라도 안게 어디야~
'iOS > Swift' 카테고리의 다른 글
[TIL / 25.04.17] URLSession으로.. 네트워크 통신을 어떻게 하는지.. 보여줄래요 (2) | 2025.04.17 |
---|---|
[TIL / 25.04.16] Lv3 서치바 관련 트러블슈팅 기록 (0) | 2025.04.16 |
[TIL / 25.03.31] 앨범에서 사진 고르고 잘라서 쓸 수 있도록! (feat. PHPickerViewController, TOCropViewController) (1) | 2025.03.31 |
[TIL / 25.03.28] 과제 5, 스택뷰가 보이지 않는 문제 해결 (2) | 2025.03.28 |
[TIL / 25.03.27] HarryPotterBooks 과제 2~4Lv 구현 및 회고 (1) | 2025.03.27 |