iOS) Collection View 와 Page Control

2021. 7. 10. 23:38iOS

구현할 것은 가로로 화면을 넘기면 페이지들이 나타남과 동시에 하단의 페이저 역시 해당 색깔로 따라가는것이다.

 

CollectionView 를 구현하기 위해서는 TableView와 마찬가지로 DataSource가 필요했다.

다른점은 TableView는 일반 UITableViewDelegate가 필요하지만 CollectionView에서는 UICollectionViewDelegateFlowLayout 이 필요했다.

 

@IBOutlet weak var listCollectionView: UICollectionView!
   
let list = [UIColor.red, UIColor.green, UIColor.blue, UIColor.gray, UIColor.black]

@IBOutlet 으로 이어진 UICollectionView 변수를 할당했고 list 에는 다섯가지 색깔을 저장했다.

 

extension PageControlViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return list.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.backgroundColor = list[indexPath.item]
        return cell
    }
}

UICollectionViewDataSource 는 UITableViewDataSource 와 마찬가지로 SubView에 몇개의 데이터가 들어가는지,

어떤 데이터가 들어가야 하는지에 대한 것이다. 또하나, 각각의 cell을 말하고 싶을때 TableView에서는 row를 CollectionView에서는 item을 쓰는듯하다.

 

코드에서 먼저 numberOfItemsInSection을 보면 list의 갯수를 센다.

그래서 이제 CollectionView에는 list.count = 5개의 cell 이 생성되겠다.

 

각각의 cell 에 무엇을 나타낼 것인지는 cellForItemAt 메서드를 사용한다.

 

문서를 보게되면 여기에서는 먼저 dequeueReuseableCell(withReuseIdentifier:for:) 을 통해서 cell의 identifier에 해당하는 유효한 cell을 찾아서 알맞게 properties를 조작한 뒤 cell을 리턴하라고 되어있다.

 

여기에서는 cell.backgroundColor = list[indexPath.item] 을 통해서 indexPath.item 에 해당하는 list 내 Color를 cell의 background 에 전달해준다.

 

extension PageControlViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return collectionView.bounds.size
    }
}

UICollectionViewDelegateFlowLayout은 처음보는 Delegate Protocol인데 문서에 따르면

 

he methods of this protocol define the size of items and the spacing between items in the grid. All of the methods in this protocol are optional. If you do not implement a particular method, the flow layout delegate uses values in its own properties for the appropriate spacing information.

이 프로토콜의 메소드들은 그리드안의 아이템 사이즈와 간격을 정의한다. 모든 메서드들은 선택적이다. 특정 메소드를 상속하지 않으면 flow layout delegate 는 적절한 간격 정보를 사용하는 프로퍼티 값을 사용한다.

 

그러니까 그리드 아이템 사이즈와 간격을 조절하는 델리게이트라는 말이다. 

여기에서 sizeForItemAt을 사용하게 되면 말그대로 Item 의 사이즈를 정의하는 메소드인 것이고 여기에서는 collectionView.bounds.size 를 리턴하므로 상위뷰인 컬렉션 뷰 사이즈(화면전체) 크기를 가져온다는 것이다. 그래서 Item 하나의 크기는 화면 전체의 크기와 같게 되었다.

 

여기까지는 CollectionView 에 대한 것이었고 pageControll은 아래 설명 참고

    @IBOutlet weak var pager: UIPageControl!

@IBOutlet 속성의 pager 변수를 만든다. 

 

override func viewDidLoad() {
  super.viewDidLoad()

  pager.numberOfPages = list.count
  pager.currentPage = 0

  pager.pageIndicatorTintColor = UIColor.systemGray
  pager.currentPageIndicatorTintColor = UIColor.systemRed
}

그리고 pager 의 기본설정을 위와같이한다. pageIndicatorTintColor 은 기본색상을 나타내며 currentPageIndicatorTintColor은 현재 페이징의 색깔을 의미한다. 

 

이제 CollectionView 가 옆으로 스크롤 될 때마다 pageControll의 색상이 바뀌게 하려면 UIScrollViewDelegate를 현재 ViewController에 채택시켜야 한다. 아, 참고로 CollectionView 또한 UIScrollView 를 상속받고 있기 때문에 이 Delegate에서 override되는 scrollView는 현재 swift 파일의 CollectionView를 의미하게 된다.

 

extension PageControlViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let width = scrollView.bounds.size.width
        // 좌표보정을 위해 절반의 너비를 더해줌
        let x = scrollView.contentOffset.x + (width/2)
        
        let newPage = Int(x / width)
        if pager.currentPage != newPage {
            pager.currentPage = newPage
        }
    }
}

scrollView.bound.size.width은 아이폰 화면의 너비와 마찬가지이다 (상위뷰인 콜렉션 뷰의 사이즈가 화면을 가득 채웠으니까) 

contentOffset은 스크롤 뷰에서 각방향으로 얼마나 움직였는지 알아내기 위한 프로퍼티이다. 그래서 한번 옆으로 움직였을때 contentOffset.x는 "0 -> 화면 가로길이 x1", 또 한번 움직였을 때 "화면 가로길이 x1 -> 화면 가로길이 x2" 이런식으로 값이 정해진다. 

이 값을 width로 나누면 페이징을 구현할 수 있다 (만약에 3번 움직였으면 scrollViewOffset.x 의 값은 화면 가로길이 x3 일테니까 여기에 화면 가로길이를 나눠주면 3)

 

그런데 강의에서는 width/2를 굳이굳이 더해주고 있다 좌표보정을 위해서라는데 일단 문의를 남겨놓은 상태다. 아무튼 나누고 newPage에 해당 값을 저장하고 pager의 현재페이지가 계산된 newPage와 다르다면 newPage를 해당 페이지의 값으로 삼는다.

 

여기까지 하면 페이지를 옆으로 슥슥 넘길때마다 페이저도 따라와지는데 pageControll를 그냥 손을 터치했을때도 화면이 넘어가게 하고 싶다면 pager에 IBAction을 추가해서 IndexPath 인스턴스를 구현한다. 현재 인덱스 패스를 구하기 위해 item argument에 sender.currentPage를 전달하고 CollectionView 의 메소드 scrollToItem(at:at:animatied:) 를 사용한다

@IBAction func pageChanged(_ sender: UIPageControl) {
        let indexPath = IndexPath(item: sender.currentPage, section: 0)
        // 인덱스패스위치로 컬렉션 뷰를 스크롤
        listCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}

이 클래스의 프로퍼티로 CollectionView 타입의 변수 listCollectionView가 있으므로 이를 이용한다. 첫번째 인자 at에는 item의 위치를, 두번째 인자 at의 .centeredHorizontally 는 UICollectionView.ScrollPosition(struct임)의  enum인데 컬렉션 뷰에서 item을 가로로 스크롤한다.