UICollectionView 공부하기 2편
이번에는 헤더랑 푸터 만들어보자
목표
- 1. 여러개의 섹션 헤더 및 푸터 만들어보기
- 2. 헤더에 이미지 넣기
- 3. 리프레시 컨트롤 넣기
- 4. 우측에 리프레시 넣기
- 5. 페이지네이션
샘플 코드
AppleCollectionView.zip
2.63MB
개발환경
swift pacakge maanger를 통해 외부 의존성 일부 사용
- RxSwift 6.5.0
- Snapkit 5.6.0
- RxGesture 4.0.4
1. 여러개의 섹션 헤더 및 푸터 만들어보기
컬렉션 뷰에서 섹션 헤더와 푸터를 만드는 만드는 방법은 아래의 메소드를 구현
섹션과 헤더의 경우에는 각 섹션마다 나타나기에 필요한 경우에만 호출되도록 구현
/// collectionView에서 헤더와 같은 역할
func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath
) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: RestaurantDetailHeaderCollectionReusableView.identifier,
for: indexPath
)
case UICollectionView.elementKindSectionFooter:
return collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: RestaurantDetailFooterCollectionReusableView.identifier,
for: indexPath
)
default:
assert(false, "dequeueReusableSupplementaryView 해당되지 않아요.")
}
}
헤더와 푸터에 들어갈 View 자체는 ReusableView를 채택해서 구현하고 각각 구현해야 함.
헤더의 경우에는 우선 viewModel이 있는데, 헤더에 광고를 넣을 예정.
요즘 ReactorKit을 학습하고 있어서 Action과 State로 bind를 나눔.
class RestaurantDetailHeaderCollectionReusableView: UICollectionReusableView {
static let identifier = String(describing: self)
let disposeBag = DisposeBag()
// MARK: - Properties
let viewModel = RestaurantDetailHeaderViewModel()
// MARK: - Views
var imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: .zero)
setupImageView()
bindAction(viewModel: viewModel)
bindState(viewModel: viewModel)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func bindAction(viewModel: RestaurantDetailHeaderViewModel) {
imageView.rx.swipeGesture([.left, .right])
.bind(to: viewModel.swipeSubject)
.disposed(by: disposeBag)
}
private func bindState(viewModel: RestaurantDetailHeaderViewModel) {
viewModel.swipeDriver
.drive { [weak self] result in
self?.imageView.image = result.0
viewModel.offsetSubject.onNext(result.1)
}
.disposed(by: disposeBag)
}
private func setupImageView() {
addSubview(imageView)
imageView.sizeToFit()
imageView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
}
class RestaurantDetailFooterCollectionReusableView: UICollectionReusableView {
static let identifier = String(describing: self)
var footerView = UIView()
override init(frame: CGRect) {
super.init(frame: .zero)
setupHeaderView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupHeaderView() {
addSubview(footerView)
footerView.backgroundColor = .green
footerView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
}
헤더와 푸터의 사이즈는 아래 메서드에서 처리
// MARK: - Header의 사이즈 -> 섹션마다 나타남
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int
) -> CGSize
// MARK: - Footer의 사이즈 -> 섹션마다 나타남
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForFooterInSection section: Int
) -> CGSize
ViewModel을 적용해보고자 함.
class RestaurantDetailHeaderViewModel {
// MARK: - Input
var swipeSubject = BehaviorSubject<UISwipeGestureRecognizer>(value: UISwipeGestureRecognizer())
var offsetSubject = BehaviorSubject<Int>(value: -1)
// MARK: - OuptPut
let swipeDriver: Driver<(UIImage, Int)>
init() {
swipeDriver = Observable.zip(swipeSubject, offsetSubject)
.debug("🚨")
.map { swipe, offset -> Int in
switch swipe.direction {
case .left:
return offset + 1
case .right: return offset - 1
default: return 0
}
}
.map { index -> (UIImage, Int) in
print("index \(index)")
if index < 0 {
return (Banner.toImage(0), 0)
}
else if index > Banner.images.count - 1 {
let index = Banner.images.count - 1
return (Banner.toImage(index), index)
}
else {
return (Banner.toImage(index), index)
}
}
.asDriver(onErrorJustReturn: (UIImage(), 0))
}
}
페이지네이션 알아보기
/// 페이지네이션
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset_y = scrollView.contentOffset.y
let contentHeight = collectionView.contentSize.height
let height = scrollView.frame.height
if contentOffset_y > (contentHeight - height) && !isLastPage {
RestaurantDetail.fetch { [weak self] restaurantDetails in
guard let self = self else { return }
self.restaurantDetails += restaurantDetails
}
}
}
'apple > iOS, UIKit, Documentation' 카테고리의 다른 글
| UICollectionView에 대해서 알아보기 3편 (동적 Cell) (0) | 2022.08.22 |
|---|---|
| iOS 시뮬레이터에서 Remote Notification (시뮬레이터 푸시알림) (0) | 2022.08.17 |
| UICollectionView 공부하기 1편 (0) | 2022.08.11 |
| warning: 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead (0) | 2022.08.10 |
| [iOS] present 이후 pushViewController (1) | 2022.08.05 |