Notice
Recent Posts
Recent Comments
Link
관리 메뉴

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

[iOS - swift] 2. UICollectionView의 DecorationView, SupplementaryView, 커스텀 Layout 본문

iOS 응용 (swift)

[iOS - swift] 2. UICollectionView의 DecorationView, SupplementaryView, 커스텀 Layout

jake-kim 2022. 3. 9. 12:49

1. UICollectionView의 SupplementaryView(HeaderView, FooterView, UICollectionReusableView)

2. UICollectionView의  DecorationView, SupplementaryView 커스텀 CollectionViewFlowLayout

DecorationView

DecorationView 이란?

  • collectionView에 Cell에 의존하지 않고 별도로 추가할 수 있는 뷰
  • DecorationView 전용 뷰를 따로 만든 후, register()해서 사용
  • UICollectionViewLayout에서 register()가 가능하므로, UICollectionViewFlowLayout이나 UICollectionViewLayout을 상속받아서 구현
  • prepare() 메소드 안에서 register()후에 UICollectionViewLayoutAttributes(forDecorationViewOfKind:with:)로 attributes로 레이아웃 정의

예제에서 UI작성의 편의를 위해 사용한 프레임워크

pod 'SnapKit'
pod 'Then'

DeocraitonView 구현 방법

  • DecorationView 커스텀 클래스 정의
    • SupplementaryView와 동일하게 `UICollectionReusableView`를 상속받아서 구현
    • 단순히 회색 배경을 가지고 있는 뷰
import UIKit
import SnapKit

final class BackgroundDecorationView: UICollectionReusableView {
  static let id = "BackgroundDecorationView"
  static var kind: String { Self.id }
  
  override var reuseIdentifier: String? {
    Self.id
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    let view = UIView()
    view.backgroundColor = .systemGray3
    view.layer.cornerRadius = 8.0
    view.layer.masksToBounds = true
    view.layer.borderColor = UIColor.clear.cgColor
    view.layer.borderWidth = 1.0
    self.addSubview(view)
    view.snp.makeConstraints {
      $0.edges.equalToSuperview()
      $0.size.equalTo(50).priority(999)
    }
  }
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
    super.apply(layoutAttributes)
  }
  override func prepareForReuse() {
    super.prepareForReuse()
    self.prepare()
  }
  
  func prepare() {
    
  }
}
  • DecorationView를 사용하려면, UICollectionViewLayout의 prepare()메소드 안에서 register()하고 레이아웃을 정의해야 하므로 커스텀
    • 델리게이트 하나 추가: 데코레이션 뷰의 CGRect값은 ViewController쪽에서 주입되도록 델리게이션
//  DecorationCollectionViewFlowLayout.swift

import UIKit

protocol DecorationCollectionViewFlowLayoutDataSource: AnyObject {
  func getDecorationViewRect(_ collectionView: UICollectionView, indexPath: IndexPath) -> CGRect
}

final class DecorationCollectionViewFlowLayout: UICollectionViewFlowLayout { 
  // TODO: register(), 데코레이션 뷰 레이아웃 설정
}
  • 캐시와 델리게이트 준비
  private var cachedDecorationViewAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
  weak var dataSource: DecorationCollectionViewFlowLayoutDataSource?
  • prepare()에서 DecorationView를 등록하고 레이아웃 계산
    • 1. register()로 DeocrationView 등록
    • 2. Section에 해당하는 Item들의 indexPath를 구하여 delegate를 통하여 VC에 DecorationView의 CGRect를 요청하여 반영
  override func prepare() {
    super.prepare()
    guard let collectionView = collectionView else { return }
    guard let dataSource = dataSource else { fatalError("Conform DecorationCollectionViewFlowLayoutDataSource") }
    self.cachedDecorationViewAttributes.removeAll()
    
    // 1. DecorationView 등록
    self.register(BackgroundDecorationView.self, forDecorationViewOfKind: BackgroundDecorationView.id)
    
    // 2. [Section, [Item]]
    let numberOfItemsListForSection = (0..<collectionView.numberOfSections)
      .map { section in return (section, collectionView.numberOfItems(inSection: section)) }
      .map { ($0, (0..<$1).map { $0 }) }
    
    numberOfItemsListForSection
      .forEach { (section, itemList) in
        itemList.forEach { [weak self] item in
          let indexPath = IndexPath(item: item, section: section)
          let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: BackgroundDecorationView.id, with: indexPath)
          attributes.frame = dataSource.getDecorationViewRect(collectionView, indexPath: indexPath)
          self?.cachedDecorationViewAttributes[indexPath] = attributes
        }
      }
  }
  • layoutAttributesForElements(in:) 메소드에서 데코레이션 뷰의 z-index값을 낮추는 작업 수행
    • 데코레이션 뷰가 아닌 일반적인 셀의 attributes를 가져오려면, super.layoutAttributesForElements(in:) 호출 (현재 UICollectionViewFlowLayout을 상속받고 있으므로)
  private enum Threshold {
    static let cellStartZIndex = 100
    static let decorationStartZIndex = 0
  }
  
  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    // 1. DecorationView가 아닌 것들(Cell)을 획득
    var array = super.layoutAttributesForElements(in: rect)
    guard self.collectionView?.numberOfSections ?? 0 > 0 else { return array }
    
    // 2. DecorationView가 아닌 것
    var cellZIndex = 0
    array?.forEach {
      $0.zIndex = cellZIndex + Threshold.cellStartZIndex
      cellZIndex += 1
    }

    // 3. DecorationView인 것
    var decorationZIndex = 0
    for (_, attributes) in self.cachedDecorationViewAttributes {
      if attributes.frame.intersects(rect){
        attributes.zIndex = decorationZIndex + Threshold.decorationStartZIndex
        array?.append(attributes)
      }
      decorationZIndex += 1
    }
    return array
  }
  • layoutAttributesForDecorationView(ofKind:at:)에서 데코레이션 attributes를 반환
  override func layoutAttributesForDecorationView(
    ofKind elementKind: String,
    at indexPath: IndexPath
  ) -> UICollectionViewLayoutAttributes? {
    self.cachedDecorationViewAttributes[indexPath]
  }

* cf) SupplementaryView도 DecorationView와 동일하게 사용하고, 단 메소드만 다른 것

  override func layoutAttributesForSupplementaryView(
    ofKind elementKind: String,
    at indexPath: IndexPath
  ) -> UICollectionViewLayoutAttributes? {
    <#code#>
  }

 

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

* 참고

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts

 

Comments