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 |