Project/WhereToGo

ScrollView 안에 PageControl 기능 넣기

밤새는 탐험가89 2024. 8. 4. 07:41

 

 

import UIKit

class HeroHeaderUIView: UIView {
    
    // MARK: - UI Components
    // 스크롤 뷰 생성 및 설정
    private let scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        scrollView.isPagingEnabled = true // 페이징 가능하도록 설정
        scrollView.showsHorizontalScrollIndicator = false // 수평 스크롤 인디케이터 숨김
        scrollView.layer.cornerRadius = 10 // 모서리를 둥글게 설정
        scrollView.isDirectionalLockEnabled = true // 스크롤 방향 잠금 설정
        return scrollView
    }()
    
    // 페이지 컨트롤 생성 및 설정
    private let pageControl: UIPageControl = {
        let pageControl = UIPageControl()
        pageControl.currentPage = 0 // 현재 페이지 초기 설정
        pageControl.pageIndicatorTintColor = .systemGray // 페이지 인디케이터 기본 색상
        pageControl.currentPageIndicatorTintColor = .white // 현재 페이지 인디케이터 색상
        return pageControl
    }()
    
    // MARK: - Variables
    private var imageViews: [UIImageView] = [] // 이미지 뷰를 담을 배열
    private let imageNames = ["church", "dosol", "gwanhanru", "hanok"] // 이미지 이름 배열
    
    // MARK: - Life Cycle
    // 초기화 함수
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(scrollView) // 스크롤 뷰 추가
        addSubview(pageControl) // 페이지 컨트롤 추가
        setupImageViews() // 이미지 뷰 설정
        scrollView.delegate = self // 델리게이트 설정
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Functions
    // 이미지 뷰 설정 함수
    private func setupImageViews() {
        // 첫 번째 및 마지막 이미지 추가를 위해 확장된 이미지 배열 생성
        let extendedImageNames = [imageNames.last!] + imageNames + [imageNames.first!]
        pageControl.numberOfPages = imageNames.count // 실제 페이지 수 설정
        
        for imageName in extendedImageNames {
            let imageView = UIImageView()
            imageView.contentMode = .scaleAspectFill // 이미지 비율 유지하며 채우기
            imageView.clipsToBounds = true // 이미지 클리핑 설정
            imageView.image = UIImage(named: imageName) // 이미지 설정
            scrollView.addSubview(imageView) // 스크롤 뷰에 이미지 뷰 추가
            imageViews.append(imageView) // 이미지 뷰 배열에 추가
        }
    }
    
    // 레이아웃 설정 함수
    override func layoutSubviews() {
        super.layoutSubviews()
        
        let padding: CGFloat = 0
        let bottomPadding: CGFloat = 40 // 테이블뷰 헤더 아래 여백
        scrollView.frame = bounds.inset(by: UIEdgeInsets(top: padding, left: padding, bottom: padding + bottomPadding, right: padding)) // 스크롤 뷰 프레임 설정
        
        pageControl.frame = CGRect(x: 0, y: bounds.height - 70, width: bounds.width, height: 30) // 페이지 컨트롤 프레임 설정
        
        for (index, imageView) in imageViews.enumerated() {
            imageView.frame = CGRect(x: CGFloat(index) * bounds.width, y: 0, width: bounds.width, height: bounds.height) // 이미지 뷰 프레임 설정
        }
        
        scrollView.contentSize = CGSize(width: bounds.width * CGFloat(imageViews.count), height: bounds.height) // 스크롤 뷰 콘텐츠 사이즈 설정
        scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false) // 초기 콘텐츠 오프셋 설정
    }
}

// MARK: - Extensions
extension HeroHeaderUIView: UIScrollViewDelegate {
    // 스크롤 뷰가 스크롤될 때 호출되는 함수
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let pageWidth = scrollView.bounds.width
        let pageIndex = round(scrollView.contentOffset.x / pageWidth)
        pageControl.currentPage = Int(pageIndex) - 1 // 실제 페이지 수는 `-1` 보정
        
        if scrollView.contentOffset.x <= 0 {
            // 첫 번째 페이지로 돌아가면 마지막 페이지로 이동
            scrollView.setContentOffset(CGPoint(x: pageWidth * CGFloat(imageViews.count - 2), y: 0), animated: false)
        } else if scrollView.contentOffset.x >= scrollView.contentSize.width - pageWidth {
            // 마지막 페이지로 돌아가면 첫 번째 페이지로 이동
            scrollView.setContentOffset(CGPoint(x: pageWidth, y: 0), animated: false)
        }
    }
}

 

 

변수 extendedImageNames 의 역할

extendedImageNames 배열은 무한 스크롤을 구현하기 위해 사용됩니다.

무한 스크롤을 구현하려면 첫 번째 이미지를 마지막에, 마지막 이미지를 첫 번째에 추가해야 합니다.

이렇게 하면 사용자가 마지막 이미지를 보고 스크롤을 계속하면 첫 번째 이미지로 부드럽게 이동할 수 있습니다.

 

private func setupImageViews() {
    // 첫 번째 및 마지막 이미지 추가
    let extendedImageNames = [imageNames.last!] + imageNames + [imageNames.first!]
    pageControl.numberOfPages = imageNames.count
    
    for imageName in extendedImageNames {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.image = UIImage(named: imageName)
        imageView.layer.cornerRadius = 10
        scrollView.addSubview(imageView)
        imageViews.append(imageView)
    }
}

 

extendedImageNames 배열은 [imageNames.last!] + imageNames + [imageNames.first!]로 생성됩니다.

이렇게 하면 마지막 이미지를 첫 번째에, 첫 번째 이미지를 마지막에 추가하여 배열이 확장됩니다.

 

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let pageWidth = scrollView.bounds.width
    let pageIndex = round(scrollView.contentOffset.x / pageWidth)
    pageControl.currentPage = Int(pageIndex) - 1 // 실제 페이지 수는 `-1` 보정
    
    if scrollView.contentOffset.x <= 0 {
        // 첫 번째 페이지로 돌아가면 마지막 페이지로 이동
        scrollView.setContentOffset(CGPoint(x: pageWidth * CGFloat(imageViews.count - 2), y: 0), animated: false)
    } else if scrollView.contentOffset.x >= scrollView.contentSize.width - pageWidth {
        // 마지막 페이지로 돌아가면 첫 번째 페이지로 이동
        scrollView.setContentOffset(CGPoint(x: pageWidth, y: 0), animated: false)
    }
}

 

이 함수는 스크롤 뷰가 스크롤될 때 호출됩니다.

사용자가 스크롤 뷰의 첫 번째 또는 마지막 페이지로 스크롤할 때, 첫 번째 또는 마지막 이미지로 부드럽게 이동하도록 설정합니다.

 

 

UIScrollView의 콘텐츠 크기를 설정하고 초기 스크롤 위치를 지정하는 역할

scrollView.contentSize = CGSize(width: bounds.width * CGFloat(imageViews.count), height: bounds.height)
scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)

 

 

scrollView.contentSize = CGSize(width: bounds.width * CGFloat(imageViews.count), height: bounds.height)

 

scrollView의 콘텐츠 크기를 설정합니다. UIScrollView는 그 안에 들어갈 콘텐츠의 전체 크기를 contentSize 속성을 통해 정의합니다.

  • width: bounds.width * CGFloat(imageViews.count):
    • bounds.width는 HeroHeaderUIView의 너비를 나타냅니다.
    • imageViews.count는 imageViews 배열에 있는 이미지 뷰의 개수를 나타냅니다.
    • 따라서 bounds.width * CGFloat(imageViews.count)는 모든 이미지 뷰를 가로로 나열할 때 필요한 총 너비를 계산합니다.
  • height: bounds.height:
    • bounds.height는 HeroHeaderUIView의 높이를 나타냅니다. 모든 이미지 뷰는 동일한 높이를 가지므로, 콘텐츠의 높이는 뷰의 높이와 동일하게 설정됩니다.

이렇게 설정하면 scrollView는 모든 이미지 뷰를 가로로 나열할 수 있는 크기를 가지게 됩니다.

 

scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)

 

scrollView의 초기 스크롤 위치를 설정합니다.

  • CGPoint(x: bounds.width, y: 0):
    • x: bounds.width는 가로로 한 페이지(이미지 뷰 하나)만큼 스크롤한 위치를 나타냅니다. 이는 무한 스크롤을 구현하기 위해 처음에 두 번째 이미지(원래 배열의 첫 번째 이미지)를 표시하도록 설정합니다.
    • y: 0은 세로 방향으로는 스크롤되지 않도록 설정합니다.
  • animated: false:
    • 이 매개변수는 스크롤 이동을 애니메이션 없이 즉시 수행하도록 설정합니다. 초기 위치를 설정하는 것이기 때문에 애니메이션을 적용하지 않습니다.

 

scrollView의 콘텐츠 크기를 설정한 후, 첫 번째 이미지 뷰가 아닌 두 번째 이미지 뷰(원래 배열의 첫 번째 이미지)를 표시하도록 스크롤 위치를 설정하는 것입니다. 이는 무한 스크롤을 구현할 때 필요한 초기 설정입니다.

첫 번째 및 마지막 이미지가 중복되어 있기 때문에, 처음에 페이지를 스크롤하면 자연스럽게 무한 스크롤처럼 보이도록 설정됩니다.

 

 

 

스크롤이 발생할 때마다 호출되며,  페이지 컨트롤과 무한 스크롤 기능을 구현하는 데 사용

extension HeroHeaderUIView: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let pageWidth = scrollView.bounds.width
        let pageIndex = round(scrollView.contentOffset.x / pageWidth)
        pageControl.currentPage = Int(pageIndex) - 1 // 실제 페이지 수는 `-1` 보정
        
        if scrollView.contentOffset.x <= 0 {
            // 첫 번째 페이지로 돌아가면 마지막 페이지로 이동
            scrollView.setContentOffset(CGPoint(x: pageWidth * CGFloat(imageViews.count - 2), y: 0), animated: false)
        } else if scrollView.contentOffset.x >= scrollView.contentSize.width - pageWidth {
            // 마지막 페이지로 돌아가면 첫 번째 페이지로 이동
            scrollView.setContentOffset(CGPoint(x: pageWidth, y: 0), animated: false)
        }
    }
}

 

extension HeroHeaderUIView: UIScrollViewDelegate

  • 이 부분은 HeroHeaderUIView 클래스가 UIScrollViewDelegate 프로토콜을 준수하도록 확장하는 것입니다.
  • 이를 통해 스크롤 뷰의 이벤트를 처리하는 메서드를 구현할 수 있습니다.

func scrollViewDidScroll(_ scrollView: UIScrollView)

  • 이 메서드는 스크롤 뷰가 스크롤될 때마다 호출됩니다. 매개변수로 스크롤된 scrollView 객체를 받습니다.

let pageWidth = scrollView.bounds.width

  • 이 줄은 각 페이지(이미지 뷰)의 너비를 계산합니다.
  • scrollView.bounds.width는 scrollView의 현재 가시 영역 너비를 나타냅니다.

let pageIndex = round(scrollView.contentOffset.x / pageWidth)

이 줄은 현재 스크롤 위치를 기준으로 페이지 인덱스를 계산합니다.

  • scrollView.contentOffset.x: 스크롤 뷰의 콘텐츠에서 가로 방향으로의 현재 스크롤 위치를 나타냅니다.
  • scrollView.contentOffset.x / pageWidth: 스크롤 위치를 페이지 너비로 나누어 현재 페이지의 부동 소수점 인덱스를 계산합니다.
  • round(...): 부동 소수점 인덱스를 가장 가까운 정수로 반올림하여 현재 페이지의 인덱스를 결정합니다.

pageControl.currentPage = Int(pageIndex) - 1

  • 이 줄은 pageControl의 현재 페이지를 업데이트합니다.
  • pageIndex는 확장된 이미지 배열의 인덱스를 기준으로 하므로, 실제 페이지 번호를 계산하기 위해 -1을 보정합니다.

if scrollView.contentOffset.x <= 0

이 조건문은 스크롤 뷰가 첫 번째 페이지(가짜 페이지)로 스크롤될 때를 감지합니다.

scrollView.setContentOffset(CGPoint(x: pageWidth * CGFloat(imageViews.count - 2), y: 0), animated: false)

이 줄은 스크롤 위치를 마지막 실제 페이지로 설정합니다.

  • pageWidth * CGFloat(imageViews.count - 2): 마지막 실제 페이지의 시작 위치를 계산합니다. imageViews.count - 2는 첫 번째 가짜 페이지와 마지막 가짜 페이지를 제외한 이미지 뷰의 수입니다.
  • animated: false: 애니메이션 없이 즉시 스크롤 위치를 변경합니다.

else if scrollView.contentOffset.x >= scrollView.contentSize.width - pageWidth

이 조건문은 스크롤 뷰가 마지막 페이지(가짜 페이지)로 스크롤될 때를 감지합니다.

scrollView.setContentOffset(CGPoint(x: pageWidth, y: 0), animated: false)

이 줄은 스크롤 위치를 첫 번째 실제 페이지로 설정합니다.

  • pageWidth: 첫 번째 실제 페이지의 시작 위치를 나타냅니다.
  • animated: false: 애니메이션 없이 즉시 스크롤 위치를 변경합니다.

요약

이 메서드는 무한 스크롤을 구현하는 데 사용됩니다. 사용자가 마지막 페이지(가짜 페이지)로 스크롤하면 첫 번째 실제 페이지로 이동하고, 첫 번째 페이지(가짜 페이지)로 스크롤하면 마지막 실제 페이지로 이동합니다. 이렇게 하면 사용자는 무한히 이미지를 스크롤할 수 있습니다. 또한, 페이지 컨트롤은 현재 표시되는 실제 페이지를 반영하도록 업데이트됩니다.