본문 바로가기
공부/[iOS&Swift]

[iOS] UICollectionView에 대하여 - Data source & Delegate

by 인생은아름다워 2022. 2. 24.

앞의 내용에 이어서 Data source와 Delegate에 대해 알아보고자 한다.

 

https://zzoo789.tistory.com/entry/iOS-UICollectionView에-대하여-Overview?category=945261  

 

 

🗒️ Designing Your Data Source and Delegate

 

모든 컬렉션뷰는 data source 객체를 가지고 있다. 이 데이터 소스 객체는 앱이 보여주는 컨텐츠 그 자체이다. 앱 데이터 모델일 수도 있고 컬렉션 뷰 컨트롤러일 수도 있다. 데이터 소스에 대한 유일한 요구사항은, 얼마나 많은 아이템이 있는지 또는 그 아이템을 그릴 때 어떤 뷰를 사용할 것인지와 같은 컬렉션 뷰가 필요로 하는 정보들을 제공하는 것이다.

delegate 객체는 필수는 아니지만 권장사항이다. 이를 통해 콘텐츠와의 상호작용을 할 수 있다. 자세한 것은 사용하면서 알아봐야겠다.

 

 


 

  • The Data Source Manages Your Content

데이터 소스는 콘텐츠를 관리하는 객체이다. 이를 정의하려면 UICollectionViewDataSource프로토콜을 채택해야 한다. 직접 채택해보면 알 수 있지만, 필수적으로 두 개의 메서드를 구현해줘야 한다.

  1. 특정 section에 몇 개의 item이 있는가?
  2. 특정 section 또는 item에 대하여 어떤 뷰를 사용해서 콘텐츠를 표시할 것인가?
  3. (선택) 컬렉션 뷰가 몇 개의 section을 가지고 있는가?

컬렉션 뷰는 1개 이상의 section을 가지고 있어야 하며, 각 section은 0개 또는 그 이상의 item을 가진다. 섹션과 아이템에 대해서는 아래의 그림을 통해 확인할 수 있다.

서로 다른 레이아웃을 통해(Flow Layout / Custom Layout : 아직 잘 모르니 일단 받아들이자...)

 

컬렉션 뷰는 NSIndexPath라는 객체를 포함하는 데이터를 보여주게 되는데, 특정 항목에 접근하고 싶을 때는 레이아웃 객체가 제공하는 IndexPath 정보를 사용하면 된다. 실제로 데이터 소스 객체에서 IndexPath로 정렬을 하더라도 실제 컬렉션 뷰가 보여주는 것은 결국 layout객체에 의해 결정된다는 사실을 잊지 말아야 한다.

 

역할이 분리되어 있음!

 

위 그림은 데이터를 효과적으로 관리하는 예시이다. Data source객체 안에 섹션의 배열과 아이템의 배열로 적절히 구분해두어(2차원 배열) 데이터 탐색 등 필요한 일이 있을 때 빠르게 접근할 수 있는 것이다.

그러면 Data source객체는 이렇게 데이터를 가지고 있다가. 언제 컬렉션 뷰 에게 전 달해 줄까에 대한 의문이 들 수 있다. 이 질문에 대해, 컬렉션 뷰는 아래의 상황에서 Data source에게 정보를 달라고 요청한다.

  1. 컬렉션 뷰가 처음 나타날 때
  2. 컬렉션 뷰에 다른 Data source를 할당할 때
  3. 컬렉션 뷰의 reloadData메서드를 직접 호출할 때
  4. 컬렉션뷰의 delegate이 perfomBatchUpdates:completion:메서드나 다른 이동, 추가, 삭제 메서드를 실행할 때

⇒ 이때마다 Date source객체는 [numberOfSectionsInCollectionView:],  [collectionView:numberOfItemsInSection:]등을 통해 컬렉션 뷰에게 몇 개의 섹션, 섹션마다 몇개의 아이템이 있는지 전달해준다. 참고로 numOfSection... 메서드는 섹션이 1개라면 구현하지 않아도 1개로 default값을 가진다고 한다.

 


  • Configuring Cells and Supplementary Views

앞서는 Data source객체가 데이터를 어떻게 관리하는지, 그리고 섹션과 아이템의 개수의 측면에서의 Data source객체를 공부했다. 또 하나의 Data source의 중요한 역할은 컬렉션 뷰가 콘텐츠를 표시하기 위해 사용할 뷰를 제공하는 것이다.

여러 차례 언급되지만, 컬렉션 뷰는 앱의 콘텐츠를 추적하지 않는다. 단지 데이터를 가져가서 레이아웃을 적용만 할 뿐이다.(레이아웃도 레이아웃 객체에서 받는다.)

 

→ 그렇기 때문에 어떻게 뷰에 그려질지는 결국 개발자가 직접 구현해줘야 하는 것이다.(데이터 소스 객체에)

 

컬렉션 뷰는 data source객체로부터 몇개의 section, item을 그려야할지를 전달받은 후에, layout 객체에게 컬렉션뷰의 컨텐츠를 그리기 위해 레이아웃 속성들을 달라고 요청한다. 컬렉션뷰는 그 속성을 바탕으로 컨텐츠들을 그리기 위해서 data source객체에게 그에 상응하는 cell과 supplement view를 요청한다. 데이터 소스 객체가 이를 제공하기 위해서는 다음의 것을 수행해야 한다.

 

  1. 스토리보드 파일에 탬플릿 cell이나 view를 추가한다.(또는 nib파일이나 class를 사용한다.)
  2. data source에서, 요청받았을 때 적절한 cell이나 view를 queue에서 빼내서 구성한다.(정확하게는 dequeue and configure라는 표현을 사용함!)

컬렉션뷰는 내부적으로 현재 사용되지 않는 cell이나 supplement view를 queue에 보관하고 있다가 필요할 때 다시 제공하여 사용한다. 이것이 reuse, recycle의 개념이다. 만약 큐에 해당하는 정보가 없다면 nil을 반환한다고 한다.

cell이나 supplement view들을 만들 때(storyboard, nib, class 등으로) 식별자를 문자열로 지정해두고 쓰면, indexPath와 함께 사용하여, 어떤 셀(또는 보충 뷰)에 어떤 뷰를 적용할지를 쉽게 설정할 수 있다.

 

그런데 이렇게 만든 Cell이나 Supplement view의 뷰의 객체를 컬렉션 뷰에 등록(register)하는 과정이 필요하고 이는 코드로 구현하려면 registerClass:forCellWithReuseIdentifier: 또는 registerNib:forCellWithReuseIdentifier: 메서드를 이용하면 된다. (이름에서 유추할 수 있듯이 하나는 Cell의 뷰를 class로 구현했을 때, 하나는 Cell의 뷰를 nib파일로 구현했을 때 사용하면 된다.)

 

Supplement View를 등록하고자 한다면 [registerClass:forSupplementaryViewOfKind:withReuseIdentifier:], 

[registerNib:forSupplementaryViewOfKind:withReuseIdentifier:]두 메서드 중에서 적절히 사용하면 된다.

 

이런 등록(register) 과정은 super view controller의 초기화 과정에서 진행해주면 된다.

예를 들어서 MainViewController에 myCollercionView라는 객체가 있다고 한다면, MainViewController내의 viewDidLoad()나 loadView() 등의 메서드에서 진행하면 될 것 같다.

 

참고로 Supplement view의 경우 아래 내용도 참고해야 하니... 나중에 사용할 때 다시 확인해보자! (kind String에 관련된 내용)

 

등록까지 완료하면, Cell이나 Supplement View는 아마 어떤 queue에 저장되어있을 것이고, 실제로 사용할 때 dequeue과정을 해줘야 하는데, 이 과정을 UICollectionViewDataSource 프로토콜을 채택함으로써 의무로 구현하게 되어있다.

 

[collectionView:cellForItemAtIndexPath:] 메서드와 [collectionView:viewForSupplementaryElementOfKind:atIndexPath:]메서드를 통해서 구현해줘야 하며, 컬렉션 뷰에서 Cell은 반드시 필요하기 때문에 위의 cell관련 메서드는 반드시 구현 해줘야 한다.

 

이 과정을 통해서 Data source는 적절한 cell 또는 supplement View를 queue에서 꺼내올 것이고, index path를 이용하여 데이터를 실제로 찾아서 뷰를 구성하고 컬렉션뷰 에게 반환하게 된다.

 

이런 과정들을 녹여서 코드로 컬렉션 뷰를 한 번 구성해봤다.

 

import UIKit

class ViewController: UIViewController {

    weak var someCollectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setupCollectionView()
        
        //3. Delegate & Data source 세팅하기 -> extension을 통해 아래에서 구현함
        self.someCollectionView.delegate = self
        self.someCollectionView.dataSource = self
        
        //4. Cell View를 사용하겠다고 register과정 -> 첫 번째 인자는 class type이 들어가야함, 두 번째는 String 으로 된 idendifier
        self.someCollectionView.register(someCollectionViewCell.self, forCellWithReuseIdentifier: someCollectionViewCell.identifier)
    }
    
    private func setupCollectionView() {
        // 1. UICollectionView생성 -> collectionViewLayout: UICollectionViewLayout() 로 했다가 정말 오랫동안 애먹었음...
        let cv = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        
        self.view.addSubview(cv)
        
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        cv.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        cv.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        cv.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        
        cv.backgroundColor = .red
        
        self.someCollectionView = cv
    }
}

// 2. class로 Cell의 View만들기
class someCollectionViewCell: UICollectionViewCell {
    //identifier를 만드는 과정
    static let identifier: String = String(describing: someCollectionViewCell.self)
    
    weak var textLabel: UILabel!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        let label = UILabel()
        self.contentView.addSubview(label)
        
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor).isActive = true
        label.backgroundColor = .blue
        
        self.textLabel = label
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    public func setupLabel(text: String) {
        self.textLabel.text = text
    }
}

extension ViewController: UICollectionViewDelegate {
    
}

extension ViewController: UICollectionViewDataSource {
    // section 개수 메서드는 의무는 아니다
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    
    // data가 없으니 일단 하드코딩
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    // 만들어놓은 cell View사용
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        //5. dequeueUseableCell을 통해
        guard let cell = self.someCollectionView.dequeueReusableCell(
            withReuseIdentifier: someCollectionViewCell.identifier,
            for: indexPath) as? someCollectionViewCell else { return UICollectionViewCell() }
        
        cell.setupLabel(text: String(indexPath.item))
        
        return cell
    }
    
    
}

 

🍎 결과 화면 정리

 

2개의 섹션, 각 섹션에는 10개의 item을 가지게 했다. 예쁘진 않아도, 일단 동작은 한다.

레이아웃이 뭔가 차곡차곡 알아서 쌓여있는 느낌이다. 이 것은 왜 이렇게 자연스럽게 레이아웃이 잡혔을까?

정답은 아마도 처음 컬렉션 뷰의 인스턴스를 생성할 때 UICollectionViewFlowLayout으로 레이아웃을 잡아줬기 때문인 것 같은데, 자세한 것은 레이아웃 공부를 하면서 다시 보도록 해야겠다.

이 뷰를 만들어내는 순서를 잘 기억해서 머리에, 손에 익을 수 있도록 해야겠다.

사실 이번 장에는 조금 더 많은 내용이 있기는 한데, 일단 기본적인 뷰를 구성하는데 의의를 두고 일단 레이아웃을 잡는 것을 공부해봐야겠다

댓글