-Preview-

PagerView

-View made today-

PagerView
CollectionView와 PageView를 사용하여 구현

-Code Review-

1. CollectionView

UICollectionView는 여러 데이터를 관리하고 커스텀 가능한 레이아웃을 사용해서 사용자에게 보여줄 수 있는 객채이다.
컬렉션 뷰는 테이블 뷰와 마찬가지로 UICollectionViewCell을 사용하여 데이터를 화면에 표현한다.
그 외에도 Section, header, footer 등을 지원하여 셀을 구분하여 표현할 수 있다.

1) CollectionView Cell 선언하기

컬렉션뷰의 아이템인 셀에 대한 값을 클래스로 선언하여 관리하기 쉽도록 한다.
셀에는 Label, indicator 값을 가지는데, 레이블은 현재 페이지를 알려주는 역할이고 인디케이터는 현재 페이지가 어디인지 더욱 보기 쉽도록 하는 이미지 뷰이다.

import UIKit

// 상단에 표시될 CollectionView의 셀 정의
class MyCollectionViewCell: UICollectionViewCell {
	// 컬렉션뷰의 셀을 재사용하기 위한 고유식별자
    static let identifier = "MyCollectionViewCell"
    
    // 컬렉션뷰의 아이템 레이블 설정
    private let label: UILabel = {
        let label = UILabel()
        label.font = UIFont.dmSans(size: 16, weight: .regular)
        label.textColor = .white
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    // 인디케이터 설정
    private let indicator: UIView = {
        let indicator = UIView()
        indicator.backgroundColor = .black
        indicator.layer.cornerRadius = 1
        indicator.translatesAutoresizingMaskIntoConstraints = false
        
        return indicator
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .black
        contentView.addSubview(label) // 레이블을 뷰에 배치
        contentView.addSubview(indicator) // 인디케이터를 뷰에 배치
        
        // 라벨, 인디케이터의 오토레이아웃 설정
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            
            indicator.heightAnchor.constraint(equalToConstant: 3),
            indicator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            indicator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            indicator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            indicator.topAnchor.constraint(equalTo: label.bottomAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 현재 탭에 따른 레이블 값 변경 함수
    func labelConfigure(with index: Int) {
        let titleIndex = index + 1
        if titleIndex == 1 {
            label.text = "소개"
        } else if titleIndex == 2 {
            label.text = "목표"
        } else if titleIndex == 3 {
            label.text = "약속"
        } else if titleIndex == 4 {
            label.text = "팀원"
        }
    }
    
    // 현재 탭과 페이지가 일치할 경우 탭에 인디케이터 표시
    func indicatorConfigure(with isSelected: Bool) {
        indicator.backgroundColor = isSelected ? UIColor.white : UIColor.black
    }
    
    // 현재 탭과 페이지가 같을 경우 레이블을 강조하는 함수
    func labelScaleChange(_ isSelected: Bool) {
        label.font = isSelected ? UIFont.dmSans(size: 18, weight: .bold) : UIFont.dmSans(size: 16, weight: .regular)
        label.textColor = isSelected ? UIColor.white : UIColor.gray
    }
}

2) PageView 선언

이제 컬렉션뷰와 상호작용하여 페이지를 보여주고, 스와이프하여 페이지를 변환할 수 있도록 도와주는 페이지뷰에 대해 class로 정의해준다.

// 페이지마다 보여질 ViewController 정의
class PageContentViewController: UIViewController {
    var pageIndex: Int = 0 // 현재 뷰 인덱스와 비교할 페이지 인덱스
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGray5
        
        // 현재 페이지 인덱스의 값에 따른 뷰 변환
        if pageIndex == 0 {
            setupCustomView(ViewController())
        } else if pageIndex == 1 {
            setupCustomView(GoalController())
        } else if pageIndex == 2 {
            setupCustomView(PromiseController())
        } else if pageIndex == 3 {
            setupCustomView(MemberController())
        }
    }
        
    // 커스텀 뷰 컨트롤러 추가
    func setupCustomView(_ vc: UIViewController) {
        let customVC = vc
        addChild(customVC)
        view.addSubview(customVC.view)
        customVC.didMove(toParent: self)
        
        // 페이지뷰의 오토레이아웃 설정
        NSLayoutConstraint.activate([
            customVC.view.topAnchor.constraint(equalTo: view.topAnchor),
            customVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            customVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            customVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }
}

3) CollectionView, PageView Controller 선언

이제 ViewController에 컬렉션뷰와 페이지 뷰를 표시하기 위한 컨트롤러를 선언한다.

class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UIPageViewControllerDelegate, UIPageViewControllerDataSource { ... }

다음으로 컨트롤러 내부에 뷰를 컨트롤할 초기 세팅을 해준다.

private var collectionView: UICollectionView!
private var pageViewController: UIPageViewController!
private var currentIndex = 0 // 현재 페이지의 인덱스값

// 페이지 뷰에 보여줄 뷰들의 배열
let pages = [ViewController(), GoalController(), PromiseController(), MemberController()]

UI를 구현하는데 상단에서부터 이미지, 탭(컬렉션), 페이지뷰의 순서이기 때문에 이미지뷰에 대한 설정을 가장 먼저 해준다.

private let imageView: UIImageView = {
	let imageView = UIImageView()
	imageView.image = UIImage(named: "hand")
	imageView.contentMode = .scaleAspectFit
	imageView.translatesAutoresizingMaskIntoConstraints = false
        
	return imageView
}()

// 이미지뷰 설정
func setupImageView() {
	view.addSubview(imageView) // 뷰에 이미지 배치
    
    // 이미지의 오토레이아웃 설정
	NSLayoutConstraint.activate([
		imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
		imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		imageView.heightAnchor.constraint(equalToConstant: 50)
	])
}

다음으로 컬렉션 뷰에 대한 설정을 해준다.

// CollectionView 설정
func setupCollectionView() {
	let layout = UICollectionViewFlowLayout() // 수평으로 아이템 정렬
	layout.scrollDirection = .horizontal // 스크롤 방향 = 수평
	layout.itemSize = CGSize(width: 100, height: 40) // 셀의 크기 설정
	layout.minimumLineSpacing = 10 // 셀 사이 간격 설정
        
	collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
	collectionView.delegate = self
	collectionView.dataSource = self
	collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: MyCollectionViewCell.identifier)
	collectionView.backgroundColor = .black
        
	view.addSubview(collectionView) // 뷰에 컬렉션뷰 배치
        
	collectionView.translatesAutoresizingMaskIntoConstraints = false
    
    // 컬렉션 뷰의 오토레이아웃 설정
	NSLayoutConstraint.activate([
		collectionView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10),
		collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		collectionView.heightAnchor.constraint(equalToConstant: 40)
	])
}

이제 페이지뷰에 대한 설정을 해준다.

// PageViewController 설정
func setupPageViewController() {
	pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
	pageViewController.delegate = self
	pageViewController.dataSource = self
        
	if let firstVC = viewControllerForPage(at: 0) {
    	// 페이지 뷰의 첫번째 페이지를 지정, 배치
		pageViewController.setViewControllers([firstVC], direction: .forward, animated: true, completion: nil)
	}
        
	addChild(pageViewController)
	view.addSubview(pageViewController.view)
	pageViewController.didMove(toParent: self)
        
	pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
    
    페이지 뷰의 오토레이아웃 설정
	NSLayoutConstraint.activate([
		pageViewController.view.topAnchor.constraint(equalTo: collectionView.bottomAnchor),
		pageViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		pageViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
		pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
	])
}

3) CollectionView Cell 설정

이제 뷰에서 컬렉션뷰의 셀의 갯수를 정하고, 셀에 담길 내용에 대한 설정을 해준다.

// CollectionView DataSource (컬렉션 뷰의 셀을 설정)
// 셀의 수 설정
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
	return pages.count
}

// 셀의 내용 설정
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
	let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.identifier, for: indexPath) as! MyCollectionViewCell
	cell.labelConfigure(with: indexPath.item)
    
    // 현재 셀 값이 페이지 값과 같은지 확인
	let isSelected: Bool = indexPath.item == currentIndex
	cell.indicatorConfigure(with: isSelected)
	cell.labelScaleChange(isSelected)
        
	return cell
}

그리고 컬렉션 뷰의 셀을 선택했을 때 페이지도 바뀔 수 있도록, 데이터소스에 대한 설정을 해준다.

// CollectionView Delegate (컬렉션 뷰에서 셀을 선택했을 때 페이지 변경)
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
	// 페이지뷰의 슬라이드 애니메이션 방향 설정
	let direction: UIPageViewController.NavigationDirection = indexPath.item > currentIndex ? .forward : .reverse
    
    // 셀의 값과 현재 페이지 값이 같지 않을 경우
    // 이미 1 페이지에 있는데 1 셀을 누르면 페이지가 슬라이드 되는 것을 방지
	if indexPath.item != currentIndex {
		if let vc = viewControllerForPage(at: indexPath.item) {
			pageViewController.setViewControllers([vc], direction: direction, animated: true, completion: nil)
			currentIndex = indexPath.item
		}
	}
	collectionView.reloadData() // 값이 변했을 때 뷰 초기화
}

4) PageView의 데이터소스와 좌우 전환 구현

페이지뷰가 셀 값에 따라 변화할 수 있도록 데이터소스를 만들고, 스와이프 했을 때 값이 변하도록 한다.

// PageViewController DataSource (이전/다음 페이지 설정)
// 이전 페이지로 돌아가기
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
	let index = (viewController as! PageContentViewController).pageIndex
	return viewControllerForPage(at: index - 1)
}

// 다음 페이지로 넘어가기
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
	let index = (viewController as! PageContentViewController).pageIndex
	return viewControllerForPage(at: index + 1)
}

그리고 페이지를 스와이프 하여 값이 변했을 때 컬렉션 뷰의 셀의 상태가 업데이트 되도록 설정한다.

// PageViewController Delegate (페이지 전환 완료 시 컬렉션뷰 선택 상태 업데이트)
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
	if completed, let currentVC = pageViewController.viewControllers?.first as? PageContentViewController {
		currentIndex = currentVC.pageIndex
		collectionView.reloadData()
		collectionView.selectItem(at: IndexPath(item: currentIndex, section: 0), animated: true, scrollPosition: .centeredHorizontally)
	}
}

이제 스토리보드의 ViewControllerclass에 만들어준 컨트롤러 뷰를 연결해준 뒤 빌드를 하면 결과물을 볼 수 있다.

5) 구현 결과물


-Today's lesson review-

오늘은 UIKit을 사용하여 컬렉션뷰와 페이지뷰를 연동하는 코드를 작성해 보았다.
미니 프로젝트에서 메인이 되는 뷰이기 때문에 그 중요도 때문에 많이 걱정도 하고 부담도 되었는데,
유튜브나 블로그 등을 여럿 돌아다니며 코드를 긁어모으니 무사히 코드를 완성할 수 있었다.
지금은 그저 코드를 따라쓰고 세부적인 내용을 모두 알지 못하지만, 하나하나 뜯어보며 공부해보고 있다.
지금의 상태로는 UI가 너무 딱딱하기 때문에 나중에는 애니메이션을 넣거나 해서 부드럽게 만들어보고 싶다.
profile
이유있는 코드를 쓰자!!

0개의 댓글