본문 바로가기

iOS/Deep dive!!!

[iOS/UIKit] UICollectionView를 왜 사용할까? CollectionView개념 완전 뿌수기+_+ | No1. 컬랙션 뷰 탐구

 

안녕하세요. 이번 포스트는 collectionView를 공부하면서 새롭게 알게된 내용, 컬랙션 뷰를 사용하는 이유 등에 대해 느낀점과 개념을 정리하려고 합니다.

 

1. When should you use CollectionView?

컬랙션 뷰를 선정하기 전에 다량의 데이터를 보여주기 좋은 UI obejct는 tableView도 있습니다. 둘 다 scrollable한 데이터를 보여줄 수 있다는 특징이 있습니다. 한 화면을 자리잡는 테이블 뷰의 경우, cell은 여러 행이 있을 수 있지만 1개의 열만 배치됩니다. 즉 행마다 1개의 cell이 자리잡습니다.

반면 collectionView는 여러 행이 존재할 수 있고, 각각의 행은 여러 개의 cell이 배치될 수 있고, 한 개의 행만 배치될 수 있습니다. 대표적으로 CollectionViewLayout을 collectionViewFlowLayout으로 설정하게 될 경우 한 화면에 배치될 수 있는 가능한 cell들을 연이어 배치합니다.

 

그럼에도 컬랙션 뷰를 왜 사용해야 하는지 의문이 들 때가 있습니다.

 

 

화면속 5개의 view 배치를 collectionView를 통해 layout하지 않을 경우, 수동으로 lay out 설정을 해야합니다. 이때 오토 레이아웃으로 각각의 view의 위치를 지정하게 된다면 오우,, 코드가 상당히 길어질 것입니다. 그래서 저는 각각의 view의 frame을 수동으로 지정해보려고 합니다.

let colors: [UIColor] = [
  .black,.blue,
  .brown,.cyan,
  .systemPink]

 

컬러를 정하고,,

class ViewController: UIViewController {
  var x = 0, y = 80
  lazy var size = CGSize(width: view.frame.width/3, height: view.frame.width/3)
  var origin: CGPoint {
    return CGPoint(x: x, y: y)
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    drawRects()
  }
}

 

각각의 뷰가 그려질 좌표와 크기를 지정합니다.

extension ViewController {
  func drawRects() {
    let views = (0...4).map{
      let frame = CGRect(origin: origin, size: size)
      let rect = UIView(frame: frame)
      updateDrawPoint()
      rect.backgroundColor = colors[$0]
      view.addSubview(rect)
    }
  }
  func updateDrawPoint() {
    x += Int(size.width)
    if x >= Int(view.frame.width) {
      x = 0; y += Int(size.width)
    }
  }
}

 

그리고 뷰들의 frame을 직접 계산해 가며 수동으로 frame 설정과 view를 추가해야 합니다. 그려야 할 뷰가 다섯 개라 쉽게 위치를 배치할 수 있었습니다.

 

데이터가 많아질 경우(뷰를 많이 그려야 할 상황)는 정말 복잡해 질 것입니다. 또한 특정 뷰를 클릭시 다른 색으로 업데이트 되야 할 때 해당 뷰의 위치를 가리킬 정보도 추가해야 합니다. 터치에 의한 액션 메서드도 추가해야 할 텐데 UIView 말고 버튼으로 바꿔야하나?,, 어마무시하게 복잡해집니다. 

 

화면에 grid로 다량의 데이터를 보여줘야 할 때, 알아서 사용자가 특정한 뷰를 클릭시 이벤트 감지를 한 후 델리게이트를 통해 특정 메서드 실행, 삭제, 동적으로 추가 뷰 삽입, 터치에 대한 selection, highlighting, view간 layout flow or compositional or custom 배치, queue를 통해 cell reuse등등. Content presentation, layout과 content manage, content dequeuing, configuring를 해주는, 이런 다양한 기능을 지원해 주는게 collectionView 입니다.

 

2. CollectionView's concept

그래서 컬랙션 뷰는 무엇인가!! objects의 조합으로 구성되어 있습니다. 크게는 컬랙션 뷰 객체지만 내부에 많은 obejct에 의해 책임이 분리되어 있습니다. 한 obejct는 데이터 담당, 다른 object는 delegate담당, 다른 object는 layout담당 등등.  컬랙션 뷰는 데이터가 정렬되고 화면에 출력되기 위한 객체입니다. Visual presentation은 많은 object로부터 관리됩니다. collection view's objects는 크게 3가지가 있습니다.

  • data source object
  • delegate object
  • layout object

 

이 개념은 4번 section: Relationship between the core objects associated with a collection view 에서 추가로 다룰 예정입니다.

 

Top-level containment and management

  • UICollectionView
  • UICollectionViewController

 

https://developer.apple.com/documentation/uikit/uicollectionview/

 

전자의 경우 UIScrollView를 상속 받았습니다. 데이터가 많을수록 해당 layout에서 지정된 scroll 방향에 따라 데이터들을 출력 가능합니다. 또한 Collection view's content를 위해 시각적 영역을 정의합니다. Content는 data source object로부터 구성되고, 각각의 content 배치는 layout object로부터 정보를 받아서 화면에 그립니다.

 

후자의 경우 collectionView를 viewController level에서 관리할 수 있게 지원됩니다.

 

CollectionView's Content management

  • UICollectionViewDataSource
  • UICollectionViewDelegate

 

CollectionView는 데이터를 수집해 보여주는 객체입니다. 데이터 관리는 data source object가 담당합니다. 이를 통해 content를 present하기 위한 views(cells)를 생성하고, collectionView's content를 관리합니다. 가장 중요하고 반드시 제공해야 하고 제공되기 위해선 해당 프로토콜을  준수해야합니다.

 

Delegate obejct의 경우 사용자로부터 발생된 여러 터치에 대한 이벤트를 감지하고 특정 delegate protocol 함수를 호출합니다. 또한 view's 동작을 커스터마이징 할 수 있게 해줍니다. 그 예로 highlighting, selection, edit 등을 추적해서 관련 메서드가 호출됩니다. 

 

DataSource는 보여 주어야 할 데이터 관련 object이므로 반드시 구현해야  합니다. 반면 delegate object는 optional 입니다.

 

Presentation

  • UICollectionReusableView
  • UICollectionViewCell

 

컬랙션 뷰는 performance를 위해 queue를 통해 views를 재사용합니다. Resue하기 위한 목적으로 dequeuing을 하는데, collectionView에 displayed된 모든 뷰들은 반드시 UICollectionReusableView 클래스 인스턴스여야 합니다.

 

https://developer.apple.com/documentation/uikit/uicollectionviewcell

 

물론 cell도 이 클래스를 상속 받습니다. cell 뿐 아니라 supplementaryView(ex header,foooter) 모두 이를 반드시 채택해야 합니다. 이를 상속받게 되면 collection view에서 displayed된 모든 뷰들은 reuse queue에서 dequeue됩니다. 이때 queue에서 삭제되는게 아닙니다. Displayed된 views가 scroll되어 화면의 bounds에서 사라지게 되면 해당 뷰들은 다시 resue queue에 들어가게 됩니다. 이 덕에 한 번 화면에 보여진 cell들은 reuse queue에서 재사용되어 화면에 보여집니다. Supplementary view를 커스텀하게 될 경우에 이를 반드시 상속받아야 합니다. 대표적으로 제가 자주 사용하는 함수는 prepareForReuse()입니다.

 

후자의 경우 reusable view의 특정 타입인데, data source object에 의해 화면에 보여질 views를 의미합니다.

 

Layout

  • UICollectionViewLayout
  • UICollectionViewLayoutAttributes
  • UICollectionViewUpdateItem

 

지금까지 위에서 언급된 개념들 중 중요한 개념은 data source, delegate입니다. data source object는 데이터들을 바탕으로 content를 생산해야 하는 책임이 있다면, delegate object는 화면에 보여진 cell을 사용자가 터치했을 때에 관련된 액션 헨들러를 처리하는 담당입니다.

 

남은 것은?....

 

 

바로 content의 배치입니다. content는 cells, suppelemntary views, decoration views가 될 수 있습니다. content의 크기는 어떻게 되는지 아직 정하지 않았습니다. tableView와 다르게 collectionView는 layout을 반드시.. 지정해야 합니다. 간단하게 아래로 cell이 구성되어 지는 tableView와 다르게 collection view는 상황에 따라 뷰의 크기가 다르게, cells가 한 행에 여러 개 배치될 수 있기 때문입니다.

 

(그럼 출력이 안될까요? 네,, 안됩니다.)

class ViewController: UIViewController {
  let CellId = "CollectionViewCell"
  let collectionView = UICollectionView()
  override func viewDidLoad() {
    super.viewDidLoad()
    setupConstraints()
    collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: CellId)
    collectionView.dataSource = self
  }
}

 

 

스토리보드에서 collectionViewController를 추가할 경우 자동으로 flowLayout이 적용됩니다. 하지만 코드 베이스로 할 때 collectionView()를 생성할 경우 layout 없이는 절대로 UICollectionView사용할 수 없습니다. 그래서 customLayout, flowLayout, compositionalLayout 중 하나를 적용해야 합니다.

 

UICollectionViewFlowLayout은 cell의 layout을 설정하지 않으면 default size(50,50)으로 화면에 보여집니다. 그리고 기본적으로UICollectionVIewLayout 디자인 목표는 이를 subclass한 FlowLayout, compositionalLayout, customLayout을 통해 다루어지도록 되어있습니다. UICollectionViewLayout은 클래스지만 virtual class? 프로토콜 느낌이라,, 생각하시면 됩니다.

 

따라서, 이 경우엔

 

...
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
...

 

이렇게 적용해야 합니다.

 

Layout object는 views location, size, visual attributes등등을 정의 할 수 있습니다. layout process 중에, layout object는 layout attribute obejcts(UICollectionViewLayoutAttributes) 인스턴스를 생성하는데, 컬랙션 뷰가 어디에, 어떻게 reuse view인 cells와 supplementary views를 display할지 말해줍니다. 

 

Layout object는 특정 views, cells가 insert, delete, collection view 내에서 drag -> drop으로 move될 때마다 ( collectionView 내에서 특정 views가 업데이트 될 때)  인스턴스로 UICollectionViewUpdateItem 타입을 받습니다. 이 또한 layout을 새로 배치해야 하기 때문입니다. (직접 생성해서는 안됩니다.)

 

UICollectionViewLayout vs UICollectionViewFlowLayout의 개념과 차이는 다음 포스트에서 자세하게 다루려고 합니다.

 

3. Apply collectionView

 

위에서 cell을 추가하지 않고 grid를 만들었습니다. 이번엔 collectionView를 통해 구현할 것입니다. CollectionView를 구성하기 위해 기본적으로 data source protocol 의  두 메서드를 사용해야 합니다. 

 

첫번째의 메서드는 collectionView의 특정 section에 대해 display될 cell의 개수를 파악하는 함수입니다. 두번째 메서드는 각각의 section에 대해서 특정 위치의 cell을 생성 또는 reuse queue에서 dequeue한 후에 cell의 내용을 구성하고 반환함으로 collectionView가 해당 cell을 화면에 display할 수 있게 해줍니다.

 

let colors: [UIColor] = [
  .black,.blue,
  .brown,.cyan,
  .systemPink]

class ViewController: UICollectionViewController {
  let CellId = "CollectionViewCell"
}

 

간편하게 UICollectionVIewController 타입으로하고 CellID라는 identifier를 정의합니다.

//MARK: - Collection view data source
extension ViewController {
  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return colors.count
  }
  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellId, for: indexPath)
    cell.backgroundColor = colors[indexPath.row]
    return cell
  } 
}

 

그리고 data source의 두 메서드를 override 해서 화면에 출력될 cell 개수를 첫번째 메서드를 통해 반환하고, 두번째 메소드를 통해 background 색을 설정합니다.

 

 

참고로 UICollectionViewController는 UICollectionViewDelegate, UICollectionViewDataSource를 진작에 채택했기 때문에 override 키워드를 통해 간편하게 프로토콜에 정의된 특정 메서드들을 사용할 수 있습니다.

 

 

layout object의 경우 필요할 경우에 커스텀이나 flow, compositional layout object를 init시점에 할당하면 됩니다. (참고로 전 스토리보드에서 추가한 UICollectionViewController를 ViewController와 연결했고, 스토리보드의 컬랙션 뷰는 UICollectionViewFlowLayout가 자동 적용됩니다.)

 

아까 위에서 layout 설명할 때 언급했지만, collectionView는 data source의 collectionView(_:numberOfItemsInSection:)를 통해 5개의 cell을 그리고 각각의 cell 스타일은 마찬가지로 data source의 collectionView(_:cellForItemAt:)을 통해 특정한 스타일의 cell을 재사용 큐를 통해 dequeue하도록 정의했습니다. 이 두 정보를 collection view가 가지고 화면에 출력을 할 것입니다.

 

 

실제 화면에 출력될 때는 일정한 size의 cell들이 출력됩니다. 기본적으로 collectionView flow layout을 지정만 한 경우에 디폴트 size(50,50) 을 지정해서 출력해 줍니다.

 

각각의 cell은 한 line에 배치 될 items간 minimum line spacing이 존재합니다. 이 경우 cell의 size가 모두 같기 때문에 actural line spacing은 minimum line spacing이 됩니다. 자세한 개념은 UICollectionViewLayout vs UICollectionViewFlowLayout에서,,

 

어쨌든.. 지금 목표는 맨 위에서 출력된 화면처럼 cell을 구성하는 것입니다. 하지만 아직 cell의 속성과, cell을 화면에 출력할 개수만 지정했다는 점입니다. 그래서 지금 해야 할 것은 layout obejct를 사용해서 각각의 cell의 사이즈, cell을 lay out을 해야 합니다. 이때 한 라인의 cell's interitem 간에 minimumspacing도 layout object를 통해 지정 하는데, 기본적인 UICollectionViewLayout은 클래스지만 프로토콜 느낌처럼 내부 멤버 변수나 함수가 구현된게 거의 없습니다. 추상클래스 느낌입니다. 

 

UICollectionViewFlowLayout은 UICollectionViewLayout을 상속받아서 추가적인 기능들(cell size, location, supplementary views(footer, header) etc..)이 지원되기 때문에, UICollectionViewFlouLayout의 UICollectionViewDelegateFlowLayout을 이용해야 합니다.

extension ViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = view.frame.width/3
    return CGSize(width: width, height: width)
  }
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 0
  }
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 0
  }
}

 

여기선 UICollectionViewDelegateFLowLayout을 채택했고 collectionView이름을 가진 함수들은 optional(선택적으로 사용할 수 있다- 모두 구현 필요x)이기 때문에 위 세 함수만 사용했습니다.

 

간략하게 설명하자면 첫번째 함수를 통해 cell의 크기를 지정하고, 두번째 함수를 통해 특정 line의 cell 중간 중간 spacing을 조정했고, 세번째 함수를 통해 line간 (행 간) line spacing을 조정했습니다.

 

 

이로써 결과는 같아졌습니다. 맨 위의 코드와 차이점은 많이 있습니다. CollectionView에서 지원되는 indexPath를 통해 특정 cell의 선택, highlighting 정보를 얻을 수 있을 뿐 아니라, delete, edit mode 등 더 많은 다양한 기능을 원할 시 override 또는 특정 프로토콜을 채택해서 사용할 수 있다는 점입니다.

 

4. Relationship between the core objects associated with a collection view

컬랙션 뷰는 크게 delegate object, data source object와 collectionViewLayout object를 통해 content를 관리하게 됩니다.

 

https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CollectionViewBasics/CollectionViewBasics.html#//apple_ref/doc/uid/TP40012334-CH2-SW14

 

이 그림은 모든 하위 object(layout, delegate, dataSource)는 collection view없인 안된다는 말을 의미합니다. CollectionView object 없이 layout object를 생성할 수 없습니다. 결국 각각의 obejct에서 설정된 정보는 collectionView가 사용합니다.

 

그러나 layout, delegate, data source objects로 분리한 이유는 collectionView's views를 lay out해야 할 때 layout object를 통해 단순히 size, location, views 간 appearance-related attributes만 결정하면 되기 때문입니다. data source를 건드리지 않아도 layout만 dynamically change가 가능하기 때문입니다.

 

다음 포스트는 UICollectionVIewLayout이 하는 일과 UICollectionViewFlowLayout에 대해서 공부한 개념을 정리할 것입니다.

 

 

Reference: 

https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html#//apple_ref/doc/uid/TP40012334-CH3-SW1

https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CollectionViewBasics/CollectionViewBasics.html#//apple_ref/doc/uid/TP40012334-CH2-SW14

https://medium.com/@linhairui19/step-by-step-guide-to-uicollectionviewlayout-d7e23b237539

 

https://developer.apple.com/documentation/uikit/uicollectionview