UICollectionView에 대해서 알아보기 2편
이번에 해볼 작업들
1. 여러개의 섹션 헤더 및 푸터 만들어보기
2. 헤더에 이미지 넣기
3. 리프레시 컨트롤 넣기
4. 우측에 리프레시 넣기
5. 페이지네이션
(해결하지 못한 부분)
- 헤더의 배너 부분을 pageViewController처럼 구현하고자 하였으나 아직 방법을 못찾음
- collectionView에 refreshControl을 달아서 사용자가 손을 떼는 순간에 이벤트가 들어가게 하려고 했으나 실패
입니다. 자료에 대한 소스코드 입니다.
사용한 Swift Package
- 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 해당되지 않아요.")
}
}
헤더와 푸터는 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을 넣었습니다.
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
}
}
}
여기서 약간 맘에 안드는 부분이 있는데,
contentOffset_y -> contentOffsetY로 쓰면 더욱 좋습니다.
'apple > iOS' 카테고리의 다른 글
[iOS] UICollectionView에 대해서 알아보기 3편 (동적 Cell) (0) | 2022.08.22 |
---|---|
[iOS] 시뮬레이터에서 Remote Notification (푸시알림) (0) | 2022.08.17 |
[iOS] UICollectionView에 대해서 알아보기 1편 (0) | 2022.08.11 |
[iOS] rootViewController 교체하기 (0) | 2022.08.10 |
[iOS] present 이후 pushViewController (0) | 2022.08.05 |