[iOS] Compositional Layout

RudinP·2024년 6월 22일
0

Study

목록 보기
232/258

UICollectionViewCompositionalLayout

func createBasicListLayout() -> UICollectionViewLayout { 
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                  
                                         heightDimension: .fractionalHeight(1.0))    
    let item = NSCollectionLayoutItem(layoutSize: itemSize)  
  
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                          
                                          heightDimension: .absolute(44))    
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,                                                   
                                                     subitems: [item])  
  
    let section = NSCollectionLayoutSection(group: group)    


    let layout = UICollectionViewCompositionalLayout(section: section)    
    return layout
}

NSCollectionLayoutSize

  • 아이템의 너비와 높이를 표현하는 클래스
  • NSCollectionLayoutDimension 타입의 값을 가진다.

고정 크기로 생성 방법

let absoluteSize = NSCollectionLayoutSize(widthDimension: .absolute(44),
                                         heightDimension: .absolute(44))

예상되는 크기 지정

let estimatedSize = NSCollectionLayoutSize(widthDimension: .estimated(200),
                                          heightDimension: .estimated(100))
  • 최대한 실제와 비슷한 크기로 설정하는 것이 바람직하며, 런타임 시 지정

비율로 크기 지정

let fractionalSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                           heightDimension: .fractionalWidth(0.2))
  • 기준이 되는 크기는 컨테이너의 크기
  • 아이템의 컨테이너 -> 그룹 / 그룹의 컨테이너 -> 섹션

uniformAcrossSiblings

  • iOS 17부터 사용 가능
  • 같은 그룹에 속한 아이템과 크기를 맞춤. 기준은 가장 큰 아이템

  • 기존의 estimated 방법을 쓰면 각 그룹 별 크기가 달라지지만, uniform across siblings를 사용하면 가장 큰 그룹 크기에 맞춰진다.
// Item width: To lay out 3 items horizontally, use 1/3 of the width of the group.
// Item height: To achieve a consistent height for the items, use `uniformAcrossSiblings(estimate:)`.
let itemCount = 3
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0 / CGFloat(itemCount)),
                                      heightDimension: .uniformAcrossSiblings(estimate: 50))
let item = NSCollectionLayoutItem(layoutSize: itemSize)


// Group width: To use the entire horizontal width of the section, use the full fractional width.
// Group height: To allow the group's height to grow for the items, use `estimated(_:)`.
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                       heightDimension: .estimated(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
                                               repeatingSubitem: item,
                                               count: itemCount)


let section = NSCollectionLayoutSection(group: group)

NSCollectionLayoutItem

  • collectionView에서 가장 기본적인 구조
  • 보통은 하나의 셀을 나타내지만, header나 footer을 나타내는 경우도 있다.

NSCollectionLayoutGroup

  • 하나 이상의 아이템이 포함된 컨테이너 객체
  • 기본적으로 수평, 수직으로 배치.
  • 그룹은 배치만 결정하고, 그룹 자체가 뭔가를 출력하는 것이 아님
  • 크기를 지정하는 방법은 아이템과 동일
  • 그룹의 컨테이너는 섹션이며, 섹션의 크기에 영향을 받는다.

  • 여러 그룹을 조합할 수도 있다.
  • 보통 타입 메소드로 만든다.

NSCollectionLayoutSection

  • 하나 이상의 그룹을 시각적으로 그룹핑하는 컨테이너
  • 셀의 배치가 동일한 경우, 보통 하나의 섹션만 필요한데 다양한 레이아웃을 구현하고 싶다면 섹션별로 별도의 그룹을 추가하는 방식으로 구현하면 됨.

Compositional Layout 구현

  • storyboard에서 설정하는 inset은 compositional layout이 아닌 flow layout일때 적용되는 값이므로 코드로 지정해주어야 함.
func setupLayout(){
        //너비는 가능한 너비 전체로 채우고자 하면 비율적으로 1
        let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(200))
        //아이템 크기 설정
        let item = NSCollectionLayoutItem(layoutSize: size)
        //그룹 설정
        let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item])
        //섹션 그룹 설정
        let section = NSCollectionLayoutSection(group: group)
        //inset 설정
        section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
        section.interGroupSpacing = 20
        
        let layout = UICollectionViewCompositionalLayout(section: section)
		detailCollectionView.collectionViewLayout = layout
    }

sectionProvider 구현

  • 섹션에 따라서 배치를 다르게 하는 방법이다.
  • 섹션을 여러개 사용할 시 사용하는 생성자다.
func createPerSectionLayout() -> UICollectionViewLayout {
//sectionIndex로 몇번째 섹션인지 확인 가능
// layoutEnvironment에 레리아웃과 관련된 속성들이 저장되어있음.
    let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
        layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
//sectionIndex로 분기하여 적절히 설정
        let columns = sectionIndex == 0 ? 2 : 4
        
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                             heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .absolute(44))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
                                                          subitem: item,
                                                            count: columns)
        
        let section = NSCollectionLayoutSection(group: group)
        return section
    }
    return layout
}

실제 구현

    func setupLayout(){
        let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
            switch sectionIndex{
            case 1:
                //한 줄에 두 셀을 표시하고자 하므로 0.5
                var size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(130))
                //아이템 크기 설정
                let item = NSCollectionLayoutItem(layoutSize: size)
                //그룹은 섹션 너비 전체를 채우도록
                size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(130))
                //그룹 설정. 나란히 배치하고자 하므로 horizontal
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
                //그룹 내 inset 설정
                //.fixed = 고정값 지정, .flexible = 최소 여백 지정. 현재 상황에서는 비율로 너비가 지정되므로 오차발생 가능성이 있기 때문에 flexible이 바람직함.
                group.interItemSpacing = .flexible(20)
                //섹션 그룹 설정
                let section = NSCollectionLayoutSection(group: group)
                //inset 설정
                section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
                section.interGroupSpacing = 20

                return section
            default:
                //너비는 가능한 너비 전체로 채우고자 하면 비율적으로 1
                let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(200))
                //아이템 크기 설정
                let item = NSCollectionLayoutItem(layoutSize: size)
                //그룹 설정
                let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item])
                //섹션 그룹 설정
                let section = NSCollectionLayoutSection(group: group)
                //inset 설정
                section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
                section.interGroupSpacing = 20

                return section
            }
        }
        detailCollectionView.collectionViewLayout = layout
    }

그러나, 두번째 섹션에서 셀을 한 줄에 두개씩 표시하다가 만약 홀수개일 경우에는 빈 부분이 발생한다. 이처럼 마지막 셀은 너비 전체를 채우도록 하고싶을 경우에는 그룹 안에 그룹을 넣어 별도로 설정해주어야 한다.

  1. 첫번째 라인처럼 두개를 절반으로 배치하는 그룹을 만든다. -> 한 줄에 2개
  2. 너비 전체를 채우는 아이템을 만든다. -> 한 줄에 1개
  3. 그룹과 아이템을 다시 그룹으로 묶는다.
...
case 1:
                //한 줄에 두 셀을 표시하고자 하므로 0.5
                var size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(130))
                //아이템 크기 설정
                var item = NSCollectionLayoutItem(layoutSize: size)
                //그룹은 섹션 너비 전체를 채우도록
                size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(130))
                //그룹 설정. 나란히 배치하고자 하므로 horizontal
                //첫번째 2개 셀 그룹
                var group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
                //그룹 내 inset 설정
                //.fixed = 고정값 지정, .flexible = 최소 여백 지정. 현재 상황에서는 비율로 너비가 지정되므로 오차발생 가능성이 있기 때문에 flexible이 바람직함.
                group.interItemSpacing = .flexible(20)
                //섹션 그룹 설정
                //두번째 1개 셀 그룹
                size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(130))
                item = NSCollectionLayoutItem(layoutSize: size)
                
                group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [group, item])
                //위 아래 inset 추가
                group.interItemSpacing = .flexible(20)
                
                let section = NSCollectionLayoutSection(group: group)
                //inset 설정
                section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
                section.interGroupSpacing = 20

                return section
...

다만, 이렇게 할 경우 그룹 내의 그룹과 아이템이 겹쳐지는 현상이 발생한다.

이는 첫번째 그룹에서는 horizontal로, 그리고 해당 그룹과 아이템을 묶은 최종 그룹에서는 vertical로 배치했기 때문에 스크롤의 범위는 무한대가 되고, 셀의 높이를 self-sizing으로 했기 때문에 이 상황에서 여백을 .flexible로 추가하게 되면 배치가 무너진다.


해결법은 .fixed로 바꾸면 된다.
종종 이렇게 배치가 무너지면 일단 .fixed로 바꿔보자.

profile
iOS 개발자가 되기 위한 스터디룸...

0개의 댓글