[UIKit] UICollectionView: CollectionView & UITableViewCell

Junyoung Park·2022년 10월 28일
0

UIKit

목록 보기
57/142
post-thumbnail

Collection View in Table View Cell (Swift Tutorial) - Xcode 11, iOS Development

UICollectionView: CollectionView & UITableViewCell

구현 목표

  • 컬렉션 뷰를 테이블 뷰 셀 내부에 구현

구현 태스크

  1. 테이블 뷰 구현
  2. 테이블 뷰 사용할 뷰 모델에서 가데이터 다운로드 컴바인 데이터 퍼블리셔를 통해 리턴
  3. 테이블 뷰 커스텀 셀 내 컬렉션 뷰 구현
  4. 커스텀 컬렉션 뷰를 통해 해당 데이터 모델을 바인딩

핵심 코드

import UIKit

class CustomTableViewCell: UITableViewCell {
    
    static let identifier = "CustomTableViewCell"
    private var models: [ImageModel] = []
    private let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 250, height: 250)
        layout.minimumInteritemSpacing = 5
        layout.scrollDirection = .horizontal
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        return collectionView
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setCollectionView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        collectionView.frame = contentView.bounds
    }
    
    private func setCollectionView() {
        contentView.addSubview(collectionView)
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.delegate = self
        collectionView.dataSource = self
    }
    
    func configure(with models: [ImageModel]) {
        self.models = models
        collectionView.reloadData()
    }
}

extension CustomTableViewCell: UICollectionViewDelegate {
    
}

extension CustomTableViewCell: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        models.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as? CustomCollectionViewCell else {
            return UICollectionViewCell()
        }
        let model = models[indexPath.row]
        cell.configure(with: model)
        return cell
    }
}

extension CustomTableViewCell: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 250, height: 250)
    }
}
  • 커스텀 테이블 뷰 셀 구현
  • 컬렉션 뷰를 내장
  • 뷰 모델에서 입력받은 데이터를 통해 컬렉션 뷰 UI 그리기
  • 레이아웃 델리게이트 함수를 통해 아이템 사이즈 등 지정

소스 코드

import UIKit
import Combine

class TableViewController: UIViewController {
    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
        return tableView
    }()
    private let viewModel: TableViewModel
    private var cancellables = Set<AnyCancellable>()
    
    init(viewModel: TableViewModel = TableViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        bind()
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        tableView.frame = view.bounds
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    private func bind() {
        viewModel
            .imageModelsSubject
            .receive(on: DispatchQueue.main)
            .sink { [weak self] models in
                self?.tableView.reloadData()
            }
            .store(in: &cancellables)
    }
}

extension TableViewController: UITableViewDelegate {
    
}

extension TableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as? CustomTableViewCell else {
            return UITableViewCell()
        }
        cell.configure(with: viewModel.imageModelsSubject.value)
        return cell
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 12
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 250
    }
}
  • 기본적인 테이블 뷰를 사용하는 뷰 컨트롤러
  • 뷰 모델에서 리턴하는 데이터를 구독, 컴바인 사용
  • 뷰 모델의 데이터를 테이블 뷰의 데이터 소스로 사용
import UIKit

class CustomTableViewCell: UITableViewCell {
    
    static let identifier = "CustomTableViewCell"
    private var models: [ImageModel] = []
    private let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 250, height: 250)
        layout.minimumInteritemSpacing = 5
        layout.scrollDirection = .horizontal
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        return collectionView
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setCollectionView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        collectionView.frame = contentView.bounds
    }
    
    private func setCollectionView() {
        contentView.addSubview(collectionView)
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.delegate = self
        collectionView.dataSource = self
    }
    
    func configure(with models: [ImageModel]) {
        self.models = models
        collectionView.reloadData()
    }
}

extension CustomTableViewCell: UICollectionViewDelegate {
    
}

extension CustomTableViewCell: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        models.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as? CustomCollectionViewCell else {
            return UICollectionViewCell()
        }
        let model = models[indexPath.row]
        cell.configure(with: model)
        return cell
    }
}

extension CustomTableViewCell: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 250, height: 250)
    }
}
  • 커스템 테이블 뷰 셀 구현
  • 오토 레이아웃이 아니라 프레임 기반 레이아웃 사용(layoutSubviews 오버라이드 함수)
import UIKit

class CustomCollectionViewCell: UICollectionViewCell {
    static let identifier = "CustomCollectionViewCell"
    
    private let label: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 38, weight: .bold)
        label.textAlignment = .center
        label.textColor = .label
        return label
    }()
    
    private let imageView: UIImageView = {
        let imageView = UIImageView()
        return imageView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
//        let width = contentView.frame.size.width
//        let height = contentView.frame.size.height
        imageView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        label.frame = CGRect(x: 75, y: 200, width: 50, height: 50)
    }
    
    private func setUI() {
        contentView.addSubview(label)
        contentView.addSubview(imageView)
        contentView.backgroundColor = .secondarySystemGroupedBackground
        contentView.clipsToBounds = true
    }
    
    func configure(with model: ImageModel) {
        label.text = model.text
        imageView.image = UIImage(named: model.imageName)
    }
}
  • 테이블 뷰 셀 내의 컬렉션 뷰가 셀로 사용하는 커스텀 셀
  • 라벨 및 이미지 뷰를 입력받은 모델을 통해 UI 그리기
import Foundation
import Combine
import UIKit

class TableViewModel {
    let imageModelsSubject: CurrentValueSubject<[ImageModel], Never> = .init([])
    private var cancellables = Set<AnyCancellable>()
    init() {
        sendMockData()
    }
    
    private func sendMockData() {
        let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
        var count = 0
        timer
            .receive(on: DispatchQueue.global(qos: .background))
            .map { _ in
                count += 1
            }
            .sink { [weak self] _ in
                if count > 7 {
                    timer.upstream.connect().cancel()
                } else {
                    let text = "\(count)"
                    let imageName = "dog_\(count)"
                    let imageModel = ImageModel(text: text, imageName: imageName)
                    var currentValue = self?.imageModelsSubject.value ?? []
                    currentValue.append(imageModel)
                    self?.imageModelsSubject.send(currentValue)
                }
            }
            .store(in: &cancellables)
    }
}
  • 0.5 초 단위로 가데이터를 전송하는 뷰 모델
  • 모든 데이터를 사용한 경우 타이머 퍼블리셔는 종료
import Foundation

struct ImageModel {
    let text: String
    let imageName: String
}
  • 셀 내에서 UI를 그릴 때 사용할 데이터 모델

구현 화면

profile
JUST DO IT

0개의 댓글