감자주먹밥

[IOS] CollectionView - CompositionalLayout 사용 본문

IOS/UIKit

[IOS] CollectionView - CompositionalLayout 사용

JustHm 2022. 12. 6. 22:30
728x90

처음 colllectionView를 사용했을 땐 StroyBoard에 UICollectionView DataSource, Delegate, 추상 클래스인 UICollectionViewLayout를 상속받은 UICollectionViewFlowLayout을 이용해서 크기 지정과 데이터를 관리하는 코드만 작성하였습니다.

이번에는 UICollectionViewCompositionalLayout을 사용해서 멋진 레이아웃을 만드는 법을 알아보겠습니다.

UICollectionViewCompositionalLayout

WWDC 19 에 발표되었으며, IOS13부터 사용할 수 있고 UICollectionViewLayout을 상속받아 만들어진 것입니다.

 

Apple Developer Documentation

 

developer.apple.com

레이아웃 하나를 만들기 위해서는 아래 코드가 필수적으로 필요합니다.

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
}

CompositionalLayout을 구성하려면..

  1. NSCollectionLayoutSection을 만들어줘야 합니다. 이걸 만들려면..
  2. NSCollectionLayoutGroup을 만들어줘야 합니다. 이걸 만들려면 GroupSize를 위한 LayouotSize와 같이..
  3. NSCollectionLayoutItem을 만들어줘야 합니다. Group과 마찬가지로 size를 지정합니다.

만들어진 Layout을 사용하려면 collectionViewLayout에 넣어주면 레이아웃은 완성됩니다.

collectionView.collectionViewLayout = layout()

Layout을 만드는 코드를 다시 보면 NSCollectionLayoutSize에 .fractionalWidth, .estimated, .absolute 가 있습니다.

이 메서드들의 동작 방식을 알아보겠습니다.

  • .fractionWidth(), .fractionHeight()
    • 감싸져 있는 컨테이너(group 또는 section)의 비율로 계산을 하여 적용됩니다.
  • .estimated()
    • 함수명처럼 입력값의 추정 값을 계산하여 적용됩니다.
  • .absolute()
    • 함수명처럼 입력한 값 그대로 절댓값으로 적용됩니다.

다음으로 알아볼 것은 NSCollectionLayoutGroup입니다.

생성에는 크게 3가지 방법이 있습니다.

Vertical은 그룹 안 아이템을 세로로 배열한다. Horizontal은 그룹 안 아이템을 가로로 배열한다. 의 차이점 이 있습니다.

custom은 해당 항목에 대한 사용자 지정 배열을 만드는 항목 공급자와 함께 지정된 크기의 그룹을 만듭니다.라고 설명돼있긴 하는데..

CustomItemProvider도 있고 해서 다음에 직접 해보고 세 번째 정리를 해봐야겠습니다..

다음으로 Group의 Property를 알아보겠습니다.

subitems의 경우 초기화 구문에서도 할 수 있는 그룹에 아이템을 넣을 수 있습니다.

supplementaryItems는 그룹에 badge, header 등을 넣을 수 있습니다. SupplementaryItem의 자세한 내용은 아래에서 다시 설명하겠습니다.

interItemSpacing은 그룹 내 아이템의 간격을 설정할 수 있습니다. 

NSCollectionLayoutEdgeSpacing(leading: .fixed(10), top: .flexible(10), trailing: nil, bottom: nil)

내부에. fixed는 위에서 썻던 .absolute와 같고, .flexible은 .estimated와 같습니다.

NSCollectionLayoutItem에도 edgeSpacing, contentInset과 같은 Property가 있어 아이템에서도 간격을 설정해줄 수 있습니다.

이번에는 NSCollectionLayoutSection을 알아보겠습니다.

생성은 처음 봤던 코드처럼 만들어 놓은 그룹을 NSCollectionLayoutSection에 넣어주면 끝입니다.

다음으로 Property를 보겠습니다. item, group보다 많은 Property를 가지고 있지만, 사용했던 몇 가지만 정리해보겠습니다.

var interGroupSpacing: CGFloat
section 내 그룹 간 간격 설정.
 
var contentInsets: NSDirectionalEdgeInsets
section의 inset을 설정할 수 있습니다.
 
var visibleItemsInvalidationHandler: NSCollectionLayoutSectionVisibleItemsInvalidationHandler?
항목이 표시되기 직전에 섹션의 항목을 수정할 수 있도록 표시 전에 호출되는 클로저입니다.
 
var orthogonalScrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior
section의 스크롤 동작방식을 설정할 수 있습니다. default값은 .none입니다.
orthogonal이란 단어는 직각 이라는 뜻, 기본 스크롤 축인 vertical의 반대인 horizontal로 스크롤을 할 수 있게 만들어줍니다.
 
옵션은 6가지가 있습니다.
  • none - 활성화 안 된 상태
  • continuous - 연속 스크롤
  • continuousGroupLeadingBoundary - 그룹의 leading에 맞춰 자연스럽게 멈추는 연속 스크롤
  • paging - 페이지 넘기듯 넘어가는 스크롤
  • groupPaging - 한 그룹씩 페이징 스크롤
  • groupPagingCentered - 각 그룹의 중심을 기준으로 페이징 스크롤
 
var boundarySupplementaryItems: [NSCollectionLayoutBoundarySupplementaryItem]
section에 header, footer 등의 SupplementaryItem을 넣을 수 있습니다.
 
var decorationItems: [NSCollectionLayoutDecorationItem]
section에 배경을 추가할 수 있습니다.

여기에 DecorationItem과 SupplementaryItem은 NSCollectionLayoutItem을 상속받아 만들어졌습니다.

struct ElementKind {
    static let badge = "badge-element-kind"
    static let background = "background-element-kind"
    static let sectionHeader = "section-header-element-kind"
    static let sectionFooter = "section-footer-element-kind"
    static let layoutHeader = "layout-header-element-kind"
    static let layoutFooter = "layout-footer-element-kind"
}

 

위와 같은 종류로 나뉘어 있는데 background는 decorationItem으로 생성할 수 있고, 나머지는 SupplementaryItem으로 생성이 가능합니다. 그리고 ElementKind에 String 값은 정해진 게 아니라 정하는 것이어서 어떤 값이든 통일되게만 사용하면 됩니다.

이 정도로 알아보고 실습을 해보겠습니다.

결과물

이런 화면을 만들어 보려고 CollectionView를 제대로 공부해봤습니다

Section, Badge의 경우 만드는 코드는 간단합니다.

    // create badge
    private func createGroupBadge() -> NSCollectionLayoutSupplementaryItem {
        let layoutSize = NSCollectionLayoutSize(widthDimension: .estimated(24), heightDimension: .estimated(24))
        let anchor = NSCollectionLayoutAnchor(edges: [.top, .trailing], fractionalOffset: CGPoint(x: 0, y: -1))
        let badge = NSCollectionLayoutSupplementaryItem(layoutSize: layoutSize, elementKind: ElementKind.badge, containerAnchor: anchor)
        return badge
    }
    // sectionHeader Layout settings
    private func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
        let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(30))
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: ElementKind.sectionHeader, alignment: .top)
        
        return sectionHeader
    }

Layout을 만들면서 item에 Badge를 넣고 section에 Header를 넣어 준 채로 반환하는 함수를 만들었습니다.

private func boxOfficeSection() -> NSCollectionLayoutSection {
        // layout
        let layoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(0.8))
	// item에 뱃지 추가
	let item = NSCollectionLayoutItem(layoutSize: layoutSize,supplementaryItems: [createGroupBadge()])
        item.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 5, bottom: 0, trailing: 5)
        
        // group
        let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(500), heightDimension: .estimated(200))
        // deprecate 예정..
        //let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        // section
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 15, trailing: 5)
       
        let sectionHeader = createSectionHeader()
        //헤더 추가
        section.boundarySupplementaryItems = [sectionHeader]
        return section
    }

만들어진 section을 Layout에 감싸 보내주면 끝입니다.

collectionView.register(BoxOfficeCell.self, forCellWithReuseIdentifier: "BoxOfficeCell")
collectionView.register(CollectionViewHeader.self, forSupplementaryViewOfKind: ElementKind.sectionHeader, withReuseIdentifier: "CollectionViewHeader")
collectionView.register(BoxOfficeBadge.self, forSupplementaryViewOfKind: ElementKind.badge, withReuseIdentifier: "BoxOfficeBadge")
collectionView.collectionViewLayout = layout()
        
private func layout() -> UICollectionViewCompositionalLayout {
	return UICollectionViewCompositionalLayout(section: boxOfficeSection())
}

layout이 다르지 않고 똑같은 것을 사용한다면 바로 생성해서 보내면 되지만 다양한 layout을 사용해야 할 때는 CompositionalLayout의 생성자에 Closure를 사용하면 됩니다.

private func layout() -> UICollectionViewLayout {
        return UICollectionViewCompositionalLayout {[weak self] sectionNumber, environment -> NSCollectionLayoutSection? in
            
            switch self?.contents[sectionNumber].sectionType {
            case .main:
                return self?.createMainTypeSection()
            case .basic:
                return self?.createBasicTypeSection()
            case .large:
                return self?.createLargeTypeSection()
            case .rank:
                return self?.createRankTypeSection()
            }
        }
    }

추가로 cell의 경우는 cellForItemAt를 통해 생성하지만 SupplementaryItem은 이 함수를 사용해 collectionView에 넣을 수 있습니다.

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView

당연한 얘기겠지만 반환형이 UICollectionReusableView기 때문에 supplementaryItem들은 이걸 상속받아 만들어져야 합니다.

register 할 때 사용된 String이 kind에 들어오기 때문에 그걸 구분해서 헤더, 뱃지 등을 할당하고 반환해주면 됩니다.

제가 만들어본 프로젝트는 여기서 확인할 수있습니다.

https://github.com/JustHm/PocketMovie

 

GitHub - JustHm/PocketMovie: 영화 API를 활용한 영화 검색 어플

영화 API를 활용한 영화 검색 어플. Contribute to JustHm/PocketMovie development by creating an account on GitHub.

github.com


layout만 간단하게 사용하는 방법을 공부해봤는데, collectionView를 이용해 여러 가지를 만들어 놓은 예제도 있어 이 예제를 보면서 추가적인 부분을 더 공부해야겠다.

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

 

Apple Developer Documentation

 

developer.apple.com

 

728x90
Comments