관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. UICollectionViewCompositionalLayout - 둘러보기 (SupplementaryView, Header, Footer) 본문

iOS 응용 (swift)

[iOS - swift] 2. UICollectionViewCompositionalLayout - 둘러보기 (SupplementaryView, Header, Footer)

jake-kim 2022. 4. 20. 23:29

1. UICollectionViewCompositionalLayout - 개념 (section, group, item)

2. UICollectionViewCompositionalLayout - 둘러보기1 (SupplementaryView, Header, Footer)

3. UICollectionViewCompositionalLayout - 개념 (DecorationView, Badge)

4. UICollectionViewCompositionalLayout - 개념 (orthogonalScrollingBehavior,  수평 스크롤, visibleItemsInvalidationHandler, NSCollectionLayoutAnchor)

5. UICollectionViewCompositionalLayout - 응용 (유튜브 뮤직 앱 UI 구현)

SupplementaryView와 DecorationView

https://ebookreading.net/view/book/EB9781484212424_13.html

  • 애플에서의 개념으로 이해하면, UICollectionView는 크게 3가지로 존재
    • item cell: 메인 데이터가 있는 UI
    • supplementaryView: 메인 데이터를 보충해주는 UI
    • decorationView: 데이터를 표출하지 않는 단순 UI (컬렉션 뷰의 모델과 독립적인 뷰) - 배경, 섹션 하이라이트에 사용
  • SupplementaryView
    • header, footer도 supplementaryView 중 하나이고 아래처럼 section을 기준으로 대표적으로 위와 아래에 위치할 수 있는 뷰
    • compositionalLayout을 사용하면 한 섹션을 기준으로 9가지의 방향으로 표현이 가능

  • DecodationView
    • 데이터를 표출하지 않고 collectionView전체에 관한 단순 UI

Header, Footer 사용 방법

  • Header나 Footer에 들어갈 뷰 준비
    • UICollectionReusableView를 상속받은 커스텀 뷰
import UIKit

final class MyHeaderFooterView: UICollectionReusableView {
  private lazy var label: UILabel = {
    let label = UILabel()
    label.textColor = .white
    label.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(label)
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    self.backgroundColor = .gray
    NSLayoutConstraint.activate([
      self.label.centerYAnchor.constraint(equalTo: self.centerYAnchor),
      self.label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
    ])
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    self.prepare(text: nil)
  }
  
  func prepare(text: String?) {
    self.label.text = text
  }
}
  • register 부분
    • collectionView.register(:forSupplementaryViewOfKind:withReuseIdentifier:) 사용
    • 델리게이트에서 dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)
// 등록 - collectionView
private lazy var collectionView: UICollectionView = {
  let view = UICollectionView(frame: .zero, collectionViewLayout: Self.getLayout())
  view.isScrollEnabled = true
  view.showsHorizontalScrollIndicator = false
  view.showsVerticalScrollIndicator = true
  view.scrollIndicatorInsets = UIEdgeInsets(top: -2, left: 0, bottom: 0, right: 4)
  view.contentInset = .zero
  view.backgroundColor = .clear
  view.clipsToBounds = true
  view.register(MyCell.self, forCellWithReuseIdentifier: "MyCell")
  
  view.register(MyHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "MyHeaderView")
  view.register(MyHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "MyFooterView")
  view.translatesAutoresizingMaskIntoConstraints = false
  return view
}()
  
// dataSource 델리게이트
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
  switch kind {
  case UICollectionView.elementKindSectionHeader:
    let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "MyHeaderFooterView", for: indexPath) as! MyHeaderFooterView
    return header
  case UICollectionView.elementKindSectionFooter:
    let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "MyHeaderFooterView", for: indexPath) as! MyHeaderFooterView
    return footer
  default:
    return UICollectionReusableView()
  }
}
  • compostionalLayout안에서 header, footer에 관한 레이아웃을 정의
    • NSCollectionLayoutBoundarySupplementaryItem 인스턴스를 생성
    • section.boundarySupplementaryItems에 인스턴스 주입
static func getLayout() -> UICollectionViewCompositionalLayout {
  UICollectionViewCompositionalLayout { (section, env) -> NSCollectionLayoutSection? in
    switch section {
    case 0:
      let itemFractionalWidthFraction = 1.0 / 3.0 // horizontal 3개의 셀
      let groupFractionalHeightFraction = 1.0 / 4.0 // vertical 4개의 셀
      let itemInset: CGFloat = 2.5
      
      // Item
      let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(itemFractionalWidthFraction),
        heightDimension: .fractionalHeight(1)
      )
      let item = NSCollectionLayoutItem(layoutSize: itemSize)
      item.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
      
      // Group
      let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(groupFractionalHeightFraction)
      )
      let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
      
      // Section
      let section = NSCollectionLayoutSection(group: group)
      section.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
      
      // header / footer
      let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0))
      let header = NSCollectionLayoutBoundarySupplementaryItem(
        layoutSize: headerFooterSize,
        elementKind: UICollectionView.elementKindSectionHeader,
        alignment: .top
      )
      let footer = NSCollectionLayoutBoundarySupplementaryItem(
        layoutSize: headerFooterSize,
        elementKind: UICollectionView.elementKindSectionFooter,
        alignment: .bottom
      )
      section.boundarySupplementaryItems = [header, footer]
      
      return section
    default:
      let itemFractionalWidthFraction = 1.0 / 5.0 // horizontal 5개의 셀
      let groupFractionalHeightFraction = 1.0 / 4.0 // vertical 4개의 셀
      let itemInset: CGFloat = 2.5
      
      // Item
      let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(itemFractionalWidthFraction),
        heightDimension: .fractionalHeight(1)
      )
      let item = NSCollectionLayoutItem(layoutSize: itemSize)
      item.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
      
      // Group
      let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(groupFractionalHeightFraction)
      )
      let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
      
      // Section
      let section = NSCollectionLayoutSection(group: group)
      section.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
      return section
    }
  }
}

좌측에 SupplementaryView 넣는 방법

  • 좌측에 view를 넣기 위해서 Header와 Footer처럼 register 준비 
// collectionView init 부분
view.register(MyHeaderFooterView.self, forSupplementaryViewOfKind: "MyLeftView", withReuseIdentifier: "MyLeftView")

// dataSource 델리게이트
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
  switch kind {
  case UICollectionView.elementKindSectionHeader:
    let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "MyHeaderView", for: indexPath) as! MyHeaderFooterView
    header.prepare(text: "헤더 타이틀")
    return header
  case UICollectionView.elementKindSectionFooter:
    let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "MyFooterView", for: indexPath) as! MyHeaderFooterView
    footer.prepare(text: "푸터 타이틀")
    return footer
  case "MyLeftView":
    let leftView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "MyLeftView", for: indexPath) as! MyHeaderFooterView
    leftView.prepare(text: "left 타이틀")
    return leftView
  default:
    return UICollectionReusableView()
  }
}
  • header, footer와 동일하게 NSCollectionLayoutBoundarySupplementaryView 인스턴스에서 .leading 옵션 사용
static func getLayout() -> UICollectionViewCompositionalLayout {
  UICollectionViewCompositionalLayout { (section, env) -> NSCollectionLayoutSection? in
  
  ...
  
  
  let leftSize = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .absolute(700))
  let left = NSCollectionLayoutBoundarySupplementaryItem(
    layoutSize: leftSize,
    elementKind: "MyLeftView",
    alignment: .leading
  )
  
  section.boundarySupplementaryItems = [header, footer, left]
  return section
  
  
  ...
  • 아래처럼 왼쪽에 supplementary view 생성 완료

spacing 주는 방법

  • 아래는 group의 left supplementaryView의 fractionalWidth를 0.1로 주고, group의 fractinoalWidth를 0.9로 준 상태
    • 여백이 왼쪽에 생기고, 그 여백에 leftView가 들어가는 방법?

  • group의 edgeSpacing 값을 부여
    • .flexible()값은 뷰의 크기에 따라 변하고, .fix()는 절대값
    • 왼쪽에 flexible()값을 부여하여, 왼쪽이 줄어들거나 늘어나도록 설정
    // in getLayout()
    
    // Group
    let groupSize = NSCollectionLayoutSize(
      widthDimension: .fractionalWidth(0.9),
      heightDimension: .fractionalHeight(groupFractionalHeightFraction)
    )
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
    group.edgeSpacing = NSCollectionLayoutEdgeSpacing(
      leading: .flexible(0),
      top: nil,
      trailing: nil,
      bottom: nil
    )

왼쪽에 leftView가 들어간 형태

* 전체 코드: https://github.com/JK0369/ExCompositoinalLayout2

 

* 참고

https://ebookreading.net/view/book/EB9781484212424_13.html

https://medium.com/@dn070287gav/all-what-you-need-to-know-about-uicollectionviewcompositionallayout-f3b2f590bdbe

https://developer.apple.com/documentation/uikit/nscollectionlayoutsection/3199094-orthogonalscrollingbehavior

https://www.raywenderlich.com/5436806-modern-collection-views-with-compositional-layouts

https://jwonylee.github.io/ios/collectionview-with-badge

Comments