UIPageControl
은 이름에서 느낌이 오듯이 페이징 뷰를 구성하는 클래스로, UIControl
을 상속한다. UIControl
은 UIView
의 하위 클래스인데, 말하자면 UIButton
처럼 addTarget
메소드를 갖고 있는 친구들이 여기에 속한다. 쉬운 말로 표현하자면 어떤 액션에 대한 트리거 기능을 하는 뷰라고 해야하나.
추가적으로, UITableView
와 UICollectionView
는 UIScrollView
를 상속한다. 그래서 저 친구들을 만들면 스크롤이 자동으로 생기는 것이다. 이걸 언급하는 이유는 페이징 기능을 만들 때 UIPageControl
과 UICollectionView
를 조합하는데, 컬렉션 뷰를 스크롤 할 때 페이지 컨트롤에 대한 동작을 지정하면서 UIScrollViewDelegate
를 사용하기 때문이다. 왜 컬렉션 뷰인데 스크롤 뷰 델리게이트를 쓰지? 라는 의문에 대한 답인 것이다.
바로 Xcode
를 열도록 한다. 연습용으로 생성한 프로젝트에 연습용 뷰 컨트롤러를 생성하여 그 안에 pageControl
을 선언하였다.
import UIKit
class PagingViewController: UIViewController {
private let pageControl: UIPageControl = {
let pageControl = UIPageControl()
return pageControl
}()
// ... //
}
그리고 프리뷰에 띄우기 위해 subview
에 추가하여 오토레이아웃을 화면 정가운데로 지정하였다.
import UIKit
class PagingViewController: UIViewController {
private let pageControl: UIPageControl = {
// ... //
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
pageControl.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pageControl)
NSLayoutConstraint.activate([
pageControl.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
pageControl.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
])
}
}
#Preview {
PagingViewController()
}
여기까지 지정해도, 화면에 아무것도 안뜬다. UILabel
이 text
를 안넣어주면 아무것도 안뜨듯이, 기본적으로 넣어줘야하는 프로퍼티 값들이 있다.
공식문서로 들어가 UIPageControl
은 어떤 값들을 갖고 있는지 찾아본다.
이게 모든 값들은 아니다. 우선 뷰에 뜨게 해줄 만한 기본값들을 가져왔다.
numberOfPages
: 총 페이지 개수
currentPage
: 화면에 띄워지는 페이지 index
hidesForSinglePage
:numberOfPages
가1
일 때 페이지컨트롤 뷰를 숨길지 여부의Bool
값
currentPageIndicatorTintColor
: 현재 페이지의 인디케이터 색상 지정
pageIndicatorTintColor
: 나머지 페이지의 인디케이터 색상 지정
이를 기반으로 총 3개
페이지를 가지고, 현재 페이지 컨트롤이 빨강
색인 페이지 컨트롤을 띄워보겠다.
private let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.currentPage = 0
pageControl.numberOfPages = 3
pageControl.currentPageIndicatorTintColor = .red
pageControl.pageIndicatorTintColor = .lightGray
return pageControl
}()
이제 뷰의 실체가 보인다.
우선 한 화면에 하나의 셀을 보여주는 컬렉션 뷰를 만들어본다. 셀 내부 컴포넌트는 없이 배경 색상만 각각 다르게 지정한다.
class PagingViewController: UIViewController {
private var colors: [UIColor] = [.systemMint, .systemTeal, .systemCyan, .systemBlue, .systemIndigo]
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
// 컬렉션 뷰 스크롤 방향 지정 : 가로 스크롤
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
// 일반 스크롤 대신 페이징 스크롤
collectionView.isPagingEnabled = true
// 스크롤 인디케이터 숨김
collectionView.showsHorizontalScrollIndicator = false
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "PageCell")
return collectionView
}()
// ... pageControl의 numberOfPages = colors.count로 바꿔줌 ... //
// auto layout : collection view 높이 300, 너비 superview width - 32
}
extension PagingViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PageCell", for: indexPath)
cell.backgroundColor = colors[indexPath.item]
return cell
}
}
extension PagingViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.bounds.size
}
}
컬렉션 뷰를 구현한 모습이다. 지금은 페이지 컨트롤과 컬렉션 뷰를 연결하지 않았기 때문에 컬렉션 뷰의 페이지가 넘어가도 페이지 컨트롤이 꿈쩍도 안하는 것을 볼 수 있다.
UIScrollViewDelegate
가 등장할 시간이다. 델리게이트는 스크롤 동작을 감지하여 자동으로 메소드를 호출해준다. 스크롤 뷰 공부시간이 아니므로 메소드 종류를 전부 언급하지는 않을 거고, 이 중 scrollViewDidEndDecelerating
는 스크롤 동작이 끝나서 스크롤의 움직임이 멈췄을 때 호출
되는 메소드다.
extension PagingViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = scrollView.visibleSize.width
let currentPage = Int(scrollView.contentOffset.x / pageWidth)
pageControl.currentPage = currentPage
}
}
이렇게 적용해주면 컬렉션 뷰의 페이지가 넘어갈 때마다 페이지 컨트롤의 currentPage
값이 변하게 된다. scrollView.visibleSize.width
와 scrollView.contentOffset.x
가 뭐길래 저렇게 했을 때 페이지가 적용되는 걸까? 저 값들을 디버깅 해보겠다.
scrollView.contentSize.width
: 스크롤 뷰(컬렉션 뷰)의 전체 너비 값
scrollView.visibleSize.width
: 화면 속 스크롤 뷰(컬렉션 뷰)의 너비 값
scrollView.contentOffset.x
: 원점 x로부터 스크롤이 움직이고 난 뒤 x까지의 거리
contentOffset.x
값이 index 0
일 때는 0.0
인데 한 페이지가 넘어갈 때마다 visibleSize.width
만큼 늘어난다. 머릿속으로 그림을 그리며 이해했는데, 그림 실력이 마땅치 않으므로 문자로라도 표현해보겠다.
| --- 현재 화면 --- |
| -- systemMint -- |
0 370
x
// ~ scroll ~ //
| --- 현재 화면 --- |
| -- systemMint -- || -- systemTeal -- |
0 370 740
x
// ~ scroll ~ //
| --- 현재 화면 --- |
| -- systemMint -- || -- systemTeal -- || -- systemCyan -- |
0 370 740 1110
x
여기서 화면에 한번에 다 담기지 않는 컬렉션 뷰의 전체 크기(scrollView.contentSize.width
)는 1850(=370*5)
이다.(scrollView.visibleSize.width
= 370
) 스크롤을 움직일 때마다 전체 너비에서 제일 왼쪽(leading
) 좌표 0
으로부터 화면에 보이는 뷰의 leading
좌표가 370*n
만큼 늘어나는 것이다. 그렇기 때문에 페이지 컨트롤의 currentPage
값에 offSet.x
나누기 visibleSize.width
를 넣어주는 것이다.
현재 예시 화면에서 페이지 컨트롤의 indicator
를 탭해도 아무 동작도 하지 않는다. 어떻게 하면 인디케이터를 탭했을 때 컬렉션 뷰의 페이지가 넘어가도록 할 수 있을까? UIButton
에 대해서 탭 동작을 구현할 때처럼, addTarget
을 통해 가능하다.
private lazy var pageControl: UIPageControl = {
// ... //
pageControl.addTarget(self, action: #selector(pageControlTapped(_:)), for: .valueChanged)
// ... //
}()
@objc func pageControlTapped(_ sender: UIPageControl) {
// 페이지 컨트롤의 currentPage index로 컬렉션 뷰를 스크롤 시킴
let indexPath = IndexPath(item: sender.currentPage, section: 0)
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
이 때, 바로 다음 인디케이터가 아닌 다다음 인디케이터를 눌러도 다음 페이지로만 넘어가는 것을 볼 수 있다.
클로드에게 바로 다다음 페이지로
넘어갈 수 없냐고 물어봤는데, 안된다
고 한다. UIPageControl
은 기본적으로 현재 페이지가 어디인지 보여주는 게 목적
인 컴포넌트라는 내용이다. 이름에 컨트롤
이 들어간다는 점에서 페이지 버튼처럼
누르면 그 페이지로 넘어가게 해줄 것 같은 느낌이 들었는데 의외인 부분이다. 이어진 내용에 따르면 커스터마이징을 통해 가능하게 만들 수는 있다고 한다.
class PagingViewController: UIViewControll {
// private var colors: [UIColor] = [.systemMint, .systemTeal, .systemCyan, .systemBlue, + 4번 붙여넣기]
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
// line spacing : 셀과 셀 간 가로 간격
layout.minimumLineSpacing = 8
// inter item spacing : 셀과 셀 사이의 간격
layout.minimumInteritemSpacing = 8
// ... //
}
private lazy var pageControl: UIPageControl = {
// ... //
// 하나의 페이지에 color 4개가 들어갈 예정이므로
pageControl.numberOfPages = colors.count / 4
// ... //
}()
}
extension PagingViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.bounds.width - 8) / 2
let height = (collectionView.bounds.height - 8) / 2
return CGSize(width: width, height: height)
}
}
셀 간 간격 때문에
scrollView.contentSize.width
가1850
에서1882
로 늘어있었고, 페이지가 넘어갈수록8*n
만큼 그리드가 오른쪽으로 밀려보였다.
처치 1 : UIScrollViewDelegate
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// ... //
// 코드 추가 : 각 페이지에서 ContentOffset.x 값을 8*n만큼 더 추가
collectionView.setContentOffset(CGPoint(x: Int(pageWidth) * pageControl.currentPage + 8 * pageControl.currentPage, y: 0), animated: true)
}
애니메이션 느려서 킹받는데? 이게 최선인건가? 일단 다른 방법은 모르겠다.
처치 2 : UIPageControl target action
@objc func pageControlTapped(_ sender: UIPageControl) {
// let indexPath = IndexPath(item: sender.currentPage * 4, section: 0)
// collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
let width = collectionView.bounds.width
collectionView.setContentOffset(CGPoint(x: Int(width) * sender.currentPage + (sender.currentPage * 8), y: 0), animated: true)
}
이정도면 UIPageControl
의 기본적인 구현은 이해했다고 봐도 되는 걸까? 컬렉션 뷰와 함께 적용하다보니 스크롤 뷰에 대한 이해도 조금 같이 챙겨간 느낌이다. 이번 팀 프로젝트에서 페이지 컨트롤을 적용했는데, 다 끝나고 나서야 이렇게 주말동안 공부를 한다. 다 이해하고 만들 시간도 없이 바로 구현을 해야했고(03)
정도까지 정리하고 나니 더 여기에 투자할 시간이 없었다) 바로 코드를 갖다 써서 만들다보니 완벽하게 구현하지 못 한 채 제출해야 했다. 모르는 채로 코드를 쳐야하는 좌절감을 느꼈던 한주였다. 그래도 모른 상태를 그냥 넘어갔다면 그 좌절감이 해소되지 않았을텐데 직접 하나하나 만들어보고 나니까 기분이 좋아졌다. 앞으로도 내가 이해하지 못했는데도 만들어야 하는 상황을 자주 맞닥뜨릴 것이다. 그때마다 답답함을 많이 느낄 것이므로 각오해야겠고, 점점 이렇게 학습하는 노하우가 쌓여서 배우는 속도가 빨라지길 바라본다.