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: 애니메이션 없이 즉시 스크롤 위치를 변경합니다.
요약
이 메서드는 무한 스크롤을 구현하는 데 사용됩니다. 사용자가 마지막 페이지(가짜 페이지)로 스크롤하면 첫 번째 실제 페이지로 이동하고, 첫 번째 페이지(가짜 페이지)로 스크롤하면 마지막 실제 페이지로 이동합니다. 이렇게 하면 사용자는 무한히 이미지를 스크롤할 수 있습니다. 또한, 페이지 컨트롤은 현재 표시되는 실제 페이지를 반영하도록 업데이트됩니다.
'Project > WhereToGo' 카테고리의 다른 글
네비게이션 바 가시성 수정 및 상태 표시줄 배경색 추가 (0) | 2024.08.05 |
---|