본문 바로가기

iOS/STUDY

[iOS] UICollectionReusableView (diffable datasource)

https://developer.apple.com/tutorials/app-dev-training/creating-a-progress-view

 

Apple Developer Documentation

 

developer.apple.com

오늘도 apple의 튜토리얼을 보고 정리한 내용입니다. 잘못된 내용이 있을 수도 있음을...

 

우선 UICollectionReusableView를 사용하는 이유가 무엇인지?

- 아래처럼 Header를 만들어주기 위해서

- 스크롤될 때 삭제하지 않고 재사용 큐에 배치하기 위해서

 

어떻게 UICollectionReusableView를 사용해서 커스텀 헤더를 만들 수 있는지 한번 알아보겠습니다.

 

 

UICollectionReusableView

A view that defines the behavior for all cells and supplementary views presented by a collection view.
collection view에서 제공하는 모든 셀 및 supplementary view에 대한 동작을 정의하는 view
@MainActor class UICollectionReusableView : UIView

 

여기서는 ProgressHeaderView 라는 클래스를 만들어서 헤더뷰를 커스텀 해볼 것입니다.

원의 윗부분인 upperView

아래부분인 lowerView

이 두가지를 포함하는 containerView 를 포함하고, constraint를 모두 설정해주었습니다.

 

진행률을 나타내기 위해 progress 변수도 설정해주었습니다.

 

더보기

---- layoutSubViews() 함수

- iOS 5.1 및 이전 버전에서는 아무런 작업을 하지 않는다.

- 하위 클래스는 하위 뷰의 보다 정확한 레이아웃을 수행하기 위해 필요에 따라 이 메서드를 재정의할 수 있다.

- 하위 뷰의 프레임 사각형을 직접 설정할 수 있다.

- 레이아웃의 업데이트를 강제로 실행하려면 setNeedsLayout() 메서드 호출

- 뷰의 레이아웃을 즉시 업데이트 하려면  layoutIfNeeded() 메서드를 호출한다.

 

---- View를 원으로 설정해주는 부분

masksToBounds = true : 나의 영역(Layer) 이외 영역의 Sub Layer는 그리지 않는다.

cornerRadius를 containerView width의 절반으로 설정한다.

import UIKit

class ProgressHeaderView: UICollectionReusableView {
    
    static var elementKind: String { UICollectionView.elementKindSectionHeader }
    
    var progress: CGFloat = 0 {
        didSet {
            // 현재 레이아웃이 초기화되고 업데이트된다.
            setNeedsLayout()
            heightConstraint?.constant = progress * bounds.height
            UIView.animate(withDuration: 0.2) { [weak self] in
                self?.layoutIfNeeded()
            }
        }
    }
    
    private let upperView = UIView(frame: .zero)
    private let lowerView = UIView(frame: .zero)
    private let containerView = UIView(frame: .zero)
    private var heightConstraint: NSLayoutConstraint?
    private var valueFormat: String { NSLocalizedString("%d percent", comment: "progress percentage value format") }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        prepareSubViews()
        
        isAccessibilityElement = true
        accessibilityLabel = NSLocalizedString("Progress", comment: "Progress view accessibility label")
        accessibilityTraits.update(with: .updatesFrequently)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        accessibilityValue = String(format: valueFormat, Int(progress * 100.0))
        heightConstraint?.constant = progress * bounds.height
        containerView.layer.masksToBounds = true
        containerView.layer.cornerRadius = 0.5 * containerView.bounds.width
    }
    
    private func prepareSubViews() {
        containerView.addSubview(upperView)
        containerView.addSubview(lowerView)
        addSubview(containerView)
        
        // translatesAutoresizingMaskIntoConstraints 를 false를 설정하여
        // subview의 제약조건을 수정할 수 있다.
        upperView.translatesAutoresizingMaskIntoConstraints = false
        lowerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        
        // width:height 1:1
        heightAnchor.constraint(equalTo: widthAnchor, multiplier: 1).isActive = true
        containerView.heightAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 1).isActive = true
        
        containerView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        containerView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        
        containerView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.85).isActive = true
        
        upperView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        upperView.bottomAnchor.constraint(equalTo: lowerView.topAnchor).isActive = true
        lowerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        
        upperView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        upperView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        lowerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        lowerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        
        heightConstraint = lowerView.heightAnchor.constraint(equalToConstant: 0)
        heightConstraint?.isActive = true
        
        backgroundColor = .clear
        containerView.backgroundColor = .clear
        upperView.backgroundColor = .todayProgressUpperBackground
        lowerView.backgroundColor = .todayProgressLowerBackground
        
    }
}

 

ProgressHeaderView 에서 설정한 elementKind 는 Section Header 라고 식별해주는 식별자입니다.

static var elementKind: String { UICollectionView.elementKindSectionHeader }

 

헤더뷰를 만들었으니 이제 collection view에 등록을 해주어야 합니다.

 

사실 왜 이 과정인지는...ㅠㅠ 잘 모르겠어요

 

1.

UICollectionView.SupplementaryRegistration

이라는 등록자를 만들어서

2.

diffable datasource의 supplementaryViewProvieder

를 제공하고

3.

collectionView에 헤더뷰를 제공할 때 진행률 제공

 

 

1. UICollectionView.SupplementaryRegistration 생성

우선 registration에서 사용할 handler를 만들고 만든 progressView를 ProgressHeaderView?로 선언한 headerView에 넣어줍니다.

 

이 registration hanlder에서는 supplementary view의 내용과 모양을 구성하는 방법을 지정합니다. (한마디로 커스텀...? 색상지정이나 text 설정이나..)

private func supplementaryRegistrationHandler(progressView: ProgressHeaderView, elementKind: String, indexPath: IndexPath) {
    headerView = progressView
}

 

더보기

공식문서에서는 registration에서 background color를 바꾸었는데 튜토리얼에서는 ProgressHeaderView class에서 자체적으로 바꿔주었습니다. ㅇㅅㅇ

 

[공식문서]

let headerRegistration = UICollectionView.SupplementaryRegistration
    <HeaderView>(elementKind: "Header") {
    // Handler
    supplementaryView, string, indexPath in
    supplementaryView.label.text = "\(string) for section \(indexPath.section)"
    supplementaryView.backgroundColor = .lightGray
}

 

그리고 UICollectionView.SupplementaryRegistration 을 생성합니다.

let headerRegistration = UICollectionView.SupplementaryRegistration(
    elementKind: ProgressHeaderView.elementKind, 
    handler: supplementaryRegistrationHandler)

 

2. diffable datasource.supplementaryViewProvieder

Registration을 생성한 후 데이터 소스의 SupplementaryViewProvider에서 호출하는 dequeueConfiguredReusableSupplementary(using:for:)에 전달합니다.

 

여기서 register() 함수를 호출할 필요없이 dequeueConfiguredReusableSupplementary()에 전달하면 자동으로 등록됩니다.

dataSource.supplementaryViewProvider = { supplementaryView, elementKind, indexPath in
    return self.collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}

 

+ 참고)

Important
Do not create your supplementary view registration inside a UICollectionViewDiffableDataSource.SupplementaryViewProvider closure; doing so prevents reuse, and generates an exception in iOS 15 and higher.

iOS 15이상에서 SupplementaryViewProvider 클로저 내부에 supplementary view registration을 생성하지 말라고 하네요!

 

 

3. collectionView에서 헤더뷰를 보여줄 때 진행률을 제공해준다.

willDisplaySupplementaryView

override func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
    // UICollectionReusableView -> ProgressHeaderView
    guard elementKind == ProgressHeaderView.elementKind, let progressView = view as? ProgressHeaderView else {
        return
    }
    progressView.progress = progress
}

 

 

 

 

끆.. 횡설수설 어찌저찌 등록하는 방법은 알았습니다만 diffable datasource라서 살짝 방법이 다른 것 같습니다..?

이 블로그에서는 viewForSupplementaryElementOfKind 함수를 구현하더라구요

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

출처: https://zeddios.tistory.com/998 [ZeddiOS]

 

안보고 구현할 수 있는 날이 올 때까지.. 기록..

 

'iOS > STUDY' 카테고리의 다른 글

[iOS] CAGradientLayer (그라데이션 배경)  (1) 2022.04.05
[iOS] @discardableResult  (0) 2022.04.05
[Swift] Copy-on-Write 최적화  (0) 2022.03.30
[iOS] UISegmentedControl  (0) 2022.03.26
[iOS] UISwipeActionsConfiguration  (0) 2022.03.26