はじめに
CA Tech Dojo/Challenge/JOB Advent Calendar 2019の20日目は@misakiagataが書かせていただきます。
次の日、21日目は@yawn_yawn_yawn_さんです!
私は、2019年8月にCATechDojo(Kotlin編)に参加させていただき、非常に多くの学びを得ることができました!
今回はCAのアドベントカレンダーということで、AbemaTVのUIにもあるような、コンテンツをカテゴリごとに横スクロールできるやつを作ってみようかと思います。(あ、iOSです)
完成形
こんな感じでTableViewの中にCollectionViewをのせて、縦スクロール×横スクロールできるよく見るUIです。

ざっくり概要説明
全体の階層は以下のようになっています。
- ViewController (ViewController.swift)
- TableView(ContentsTableView.swift)
- TableViewCell(ContentsViewCell.swift)
- CollectionView(ContentsCollectionView.swift)
- CollectionViewCell(ContentsCollectionViewCell.swift)
今回はTableViewCell、CollectionViewCellともにxibファイルを使っての実装となっており、それぞれのファイルの階層は以下のようになっています(左がTableViewCell.xibで右がCollectionViewCell.xibのイメージ)。

CollectionViewCell
CollectionView.xibにはUIImageViewをのせているだけです。便宜上UIImageViewにはAssets.xcasesに登録した画像をセットしています。

import UIKit
class ContentsCollectionViewCell: UICollectionViewCell {
    @IBOutlet var contentsImageView: UIImageView!
    override func awakeFromNib() {
        super.awakeFromNib()
        
        configureUI()
    }
    
    func configureUI() {
        contentsImageView.layer.cornerRadius = 4
        contentsImageView.layer.masksToBounds = true
        contentsImageView.image = UIImage(named: "yui")
    }
}
TableViewCell
TableViewCell.xib中にUICollectionViewを配置しています。

スクロールした時にバーが出現しないように以下のチェックは外してあります。また、ScrollDirectionはHorizontalにしておきましょう。

import UIKit
class ContentsTableViewCell: UITableViewCell {
    
    @IBOutlet var contentsCollectionView: UICollectionView!
    @IBOutlet var titleLabel: UILabel!
    
    var collectionViewOffset: CGFloat {
        get {
            return contentsCollectionView.contentOffset.x
        }
        
        set {
            contentsCollectionView.contentOffset.x = newValue
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        
        let nib = UINib(nibName: "ContentsCollectionViewCell", bundle: .main)
        contentsCollectionView.register(nib, forCellWithReuseIdentifier: "ContentsCollectionViewCell")
    }
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Configure the view for the selected state
    }
    
    func setCollectionViewDataSourceDelegate
        <D: UICollectionViewDataSource & UICollectionViewDelegate>
        (dataSourceDelegate: D, forRow row: Int) {
        
        contentsCollectionView.delegate = dataSourceDelegate
        contentsCollectionView.dataSource = dataSourceDelegate
        contentsCollectionView.reloadData()
    }
}
ViewController
ViewControllerでは各種デリゲートメソッドなどなどを実装しています。(便宜上一部数値をベタ書きしてます。)
import UIKit
class ContentsViewController: UIViewController {
    
    @IBOutlet var contentsTableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureUI()
    }
    
    func configureUI() {
        contentsTableView.delegate = self
        contentsTableView.dataSource = self
        let nib = UINib(nibName: "ContentsTableViewCell", bundle: .main)
        contentsTableView.register(nib, forCellReuseIdentifier: "ContentsTableViewCell")
        contentsTableView.tableFooterView = UIView()
    }
    
}
extension ContentsViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 160
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        guard let cell = cell as? ContentsTableViewCell else { return }
        cell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forRow: indexPath.section)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ContentsTableViewCell", for: indexPath) as! ContentsTableViewCell
        cell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forRow: indexPath.row)
        cell.contentsCollectionView.reloadData()
        return cell
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
}
extension ContentsViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ContentsCollectionViewCell", for: indexPath) as! ContentsCollectionViewCell
        return cell
        
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 104, height: 136)
    }
}
Storyboard
まとめ
今回はかなりシンプルなサンプル作成だったので簡略化して書いている部分も多いですが、TableViewCellの行によってセットするCollectionViewCellを変えたりして色々とカスタマイズできそうです。
参考文献
https://ashfurrow.com/blog/putting-a-uicollectionview-in-a-uitableviewcell-in-swift/
https://qiita.com/akspect/items/f996dd09cb05051e09ca


