apple/iOS, UIKit, Documentation

UICollectionView 공부하기 2편

lgvv 2022. 8. 12. 21:27

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
            }
        }
    }