관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] UITableVIewCell 안에 CollectionView 넣기 (Cell안의 UICollectionView) 본문

UI 컴포넌트 (swift)

[iOS - swift] UITableVIewCell 안에 CollectionView 넣기 (Cell안의 UICollectionView)

jake-kim 2022. 6. 1. 23:13

UITableViewCell 안의 UICollectionView

Cell안에 UICollectionView를 넣는 구현 아이디어

https://ashfurrow.com/blog/putting-a-uicollectionview-in-a-uitableviewcell-in-swift/

  • UITableViewCell 안에 UICollectionView가 있어야하므로, UITableViewCell을 서브클래싱한 커스텀 셀에 UICollectionView를 정의
  • UITableViewCell의 커스텀셀에서 dataSources를 처리하도록 구현
  • UITableViewCell에서 items들을 들고 있도록 구현

구현 - 모델링

구조

  • 셀에 표시할 타입 정의
    • thumbnail 케이스는 위 사진에서 MyTableViewCellOne에서 사용할 타입
    • collection 케이스는 위 사진에서 MyTableViewCellTwo에서 사용할 타입 (이 셀에는 collectionView가 존재하므로, collectionView에서 사용할 데이터도 포함)
    • 모델링의 핵심은 collectionView의 데이터도 같이 넘기는 것
import Foundation
import UIKit

enum MyItem: Equatable {
  case thumbnail(UIImage?, String) // thumbnailImage, name
  case collection(String, [CollectionViewItem]) // name, collection
}

enum CollectionViewItem: Equatable {
  case color(UIColor)
}
  • 예제로 사용할 데이터)
// ViewController.swift

var items: [MyItem] = [
  .thumbnail(UIImage(named: "flower"), "iOS flower"),
  .thumbnail(UIImage(named: "flower"), "iOS thumbnail"),
  .thumbnail(UIImage(named: "flower"), "iOS image"),
  .collection("<Color List>", [
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
  ]),
  .thumbnail(UIImage(named: "flower"), "iOS capture"),
  .thumbnail(UIImage(named: "flower"), "iOS development"),
  .thumbnail(UIImage(named: "flower"), "iOS jake"),
]

구현 - MyCollectionViewCell

(가장 뎁스가 깊은, MyCollectionViewCell 부터 구현)

MyTableViewCellTwo안에 UICollectionView가 가지고 있는 MyCollectionViewCell

  • UICollectionViewCell을 서브클래싱하고 UIView가 하나 존재
import UIKit

final class MyCollectionViewCell: UICollectionViewCell {
  static let id = "MyCollectionViewCell"
  
  private let colorView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  
  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    self.contentView.addSubview(self.colorView)
    
    NSLayoutConstraint.activate([
      self.colorView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor),
      self.colorView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor),
      self.colorView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
      self.colorView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
    ])
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    
    self.prepare(color: nil)
  }
  
  func prepare(color: UIColor?) {
    self.colorView.backgroundColor = color
  }
}

구현 - MyTableViewCellTwo

MyTableViewCellTwo

  • 핵심이 되는 셀이며, 해당 셀에서 UICollectionView를 가지고 있고 UICollectionViewDataSource까지 처리하는 셀
    • cellHeight까지 정의
      • 해당 셀을 사용하는 UITableViewDelegate의 heightForRowAt에서 높이 값을 주기 위함 (수평 스크롤 뷰를 위해 셀의 높이 고정)
      • 해당 셀에서 UICollectionViewFlowLayout 인스턴스의 itemSize에서 height값에 해당 값을 부여
import UIKit

final class MyTableViewCellTwo: UITableViewCell {
  static let id = "MyTableViewCellTwo"
  static let cellHeight = 300.0
}

(cellHeight값을 사용하는 ViewController)

// ViewController.swift

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  switch self.items[indexPath.item] {
  case .thumbnail:
    return UITableView.automaticDimension
  case .collection:
    return MyTableViewCellTwo.cellHeight
  }
}
  • MyTableViewCellTwo에 사용할 UI 및 레이아웃 정의
    • 핵심 부분은 Cell에서 collectionView에 데이터를 뿌려줄 items를 가지고 있고, collectionView.dataSource = self로 선언한 코드
//  MyTableViewCellTwo.swift

private let label: UILabel = {
  let label = UILabel()
  label.font = .systemFont(ofSize: 25)
  label.numberOfLines = 0
  label.textColor = .gray
  label.translatesAutoresizingMaskIntoConstraints = false
  return label
}()
private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()
  layout.scrollDirection = .horizontal
  layout.minimumLineSpacing = 8.0
  layout.minimumInteritemSpacing = 0
  layout.itemSize = .init(width: 300, height: cellHeight)
  return layout
}()
lazy var collectionView: UICollectionView = {
  let view = UICollectionView(frame: .zero, collectionViewLayout: self.collectionViewFlowLayout)
  view.isScrollEnabled = true
  view.showsHorizontalScrollIndicator = false
  view.showsVerticalScrollIndicator = true
  view.contentInset = .zero
  view.backgroundColor = .clear
  view.clipsToBounds = true
  view.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: MyCollectionViewCell.id)
  view.translatesAutoresizingMaskIntoConstraints = false
  return view
}()

private var items = [CollectionViewItem]() // <-

@available(*, unavailable)
required init?(coder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  super.init(style: style, reuseIdentifier: reuseIdentifier)

  self.collectionView.dataSource = self // <-
  
  self.contentView.addSubview(self.label)
  self.contentView.addSubview(self.collectionView)

  NSLayoutConstraint.activate([
    self.label.leftAnchor.constraint(equalTo: self.contentView.leftAnchor),
    self.label.rightAnchor.constraint(equalTo: self.contentView.rightAnchor),
    self.label.topAnchor.constraint(equalTo: self.contentView.topAnchor),
  ])
  NSLayoutConstraint.activate([
    self.collectionView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor),
    self.collectionView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor),
    self.collectionView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
    self.collectionView.topAnchor.constraint(equalTo: self.label.bottomAnchor),
  ])
}
  • prepare에서 collectionView의 items도 받도록 구현
//  MyTableViewCellTwo.swift

override func prepareForReuse() {
  super.prepareForReuse()
  self.prepare(name: nil, items: [])
}

func prepare(name: String?, items: [CollectionViewItem]) {
  self.label.text = name
  self.items = items
}
  • dataSource 처리
//  MyTableViewCellTwo.swift

extension MyTableViewCellTwo: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    self.items.count
  }
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch self.items[indexPath.item] {
    case let .color(color):
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.id, for: indexPath) as! MyCollectionViewCell
      cell.prepare(color: color)
      return cell
    }
  }
}

구현 - MyTableViewCellOne

MyTableViewCellOne

(특징이 없고 일반적인 UITableViewCell이므로 생략)

구현 - 사용하는 ViewController 쪽

  • tableView 정의
//  ViewController.swift

import UIKit

class ViewController: UIViewController {
  private let tableView: UITableView = {
    let view = UITableView()
    view.allowsSelection = false
    view.backgroundColor = .clear
    view.separatorStyle = .none
    view.bounces = true
    view.showsVerticalScrollIndicator = true
    view.contentInset = .zero
    view.register(MyTableViewCellOne.self, forCellReuseIdentifier: MyTableViewCellOne.id)
    view.register(MyTableViewCellTwo.self, forCellReuseIdentifier: MyTableViewCellTwo.id)
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  ... 
}
  • 테스트로 사용할 데이터 선언
var items: [MyItem] = [
  .thumbnail(UIImage(named: "flower"), "iOS flower"),
  .thumbnail(UIImage(named: "flower"), "iOS thumbnail"),
  .thumbnail(UIImage(named: "flower"), "iOS image"),
  .collection("<Color List>", [
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
    .color(randomColor),
  ]),
  .thumbnail(UIImage(named: "flower"), "iOS capture"),
  .thumbnail(UIImage(named: "flower"), "iOS development"),
  .thumbnail(UIImage(named: "flower"), "iOS jake"),
]
  • tableView 데이터 소스 처리
    • 일반적인 테이블 뷰 데이터 소스처리하는 방식이랑 동일
    • 핵심은 collectionView가 있는 tableViewCell에는 collectionView에 사용할 item 배열을 넘겨주는 것
      • cell.prepare(name: name, items: items)
    • collectionView가 있는 tableViewCell은 수평 스크롤로 구현하려고 했으므로, heightForRowAt메소드에서 위에서 정했던 cellHeight값을 반환
extension ViewController: UITableViewDataSource, UITableViewDelegate {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    self.items.count
  }
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    switch self.items[indexPath.item] {
    case let .thumbnail(image, name):
      let cell = tableView.dequeueReusableCell(withIdentifier: MyTableViewCellOne.id, for: indexPath) as! MyTableViewCellOne
      cell.prepare(image: image, name: name)
      return cell
    case let .collection(name, items):
      let cell = tableView.dequeueReusableCell(withIdentifier: MyTableViewCellTwo.id, for: indexPath) as! MyTableViewCellTwo
      cell.prepare(name: name, items: items)
      return cell
    }
  }
  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    switch self.items[indexPath.item] {
    case .thumbnail:
      return UITableView.automaticDimension
    case .collection:
      return MyTableViewCellTwo.cellHeight
    }
  }
}

* 전체 코드: https://github.com/JK0369/ExCollectionViewInCell

Comments