ios 8 Swift - TableView with embedded CollectionView

Details

  • Xcode 10.2.1 (10E1001), Swift 5

Full sample

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    fileprivate var tableViewCellCoordinator: [Int: IndexPath] = [:]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        tableView.tableFooterView = UIView()
    }
}

// UITableViewDataSource

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 5
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CollectionViewTableViewCell") as! CollectionViewTableViewCell
        cell.selectionStyle = .none
        cell.collectionView.delegate = self
        cell.collectionView.dataSource = self

        let tag = tableViewCellCoordinator.count
        cell.collectionView.tag = tag
        tableViewCellCoordinator[tag] = indexPath

        return cell
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "section: \(section)"
    }

}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        let cell = cell as! CollectionViewTableViewCell
        cell.collectionView.reloadData()
        cell.collectionView.contentOffset = .zero
    }
}

// UICollectionViewDataSource

extension ViewController: UICollectionViewDataSource {

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell

        var text = ""
        if let indexPathOfCellInTableView = tableViewCellCoordinator[collectionView.tag] {
            text = "\(indexPathOfCellInTableView)"
        }
        cell.label.text = text + " \(indexPath)"
        return cell
    }
}

// UICollectionViewDelegate

extension ViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("selected collectionViewCell with indexPath: \(indexPath) in tableViewCell with indexPath: \(tableViewCellCoordinator[collectionView.tag]!)")
    }
}

CollectionViewTableViewCell

import UIKit

class CollectionViewTableViewCell: UITableViewCell {

    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout!
}

CollectionViewCell

import UIKit

class CollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var label: UILabel!
}

Main.storyboard

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
    <device id="retina4_7" orientation="portrait">
        <adaptation id="fullscreen"/>
    </device>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
        <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackoverflow_31582378" customModuleProvider="target" sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="200" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="pS5-CW-ipl">
                                <rect key="frame" x="0.0" y="20" width="375" height="647"/>
                                <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                                <prototypes>
                                    <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="CollectionViewTableViewCell" id="bMP-Ac-C8D" customClass="CollectionViewTableViewCell" customModule="stackoverflow_31582378" customModuleProvider="target">
                                        <rect key="frame" x="0.0" y="28" width="375" height="200"/>
                                        <autoresizingMask key="autoresizingMask"/>
                                        <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="bMP-Ac-C8D" id="mcy-FO-bcc">
                                            <rect key="frame" x="0.0" y="0.0" width="375" height="199"/>
                                            <autoresizingMask key="autoresizingMask"/>
                                            <subviews>
                                                <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="yY4-ue-1HX">
                                                    <rect key="frame" x="8" y="8" width="359" height="183"/>
                                                    <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                                                    <collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="10" id="pPl-9q-MGc">
                                                        <size key="itemSize" width="180" height="180"/>
                                                        <size key="headerReferenceSize" width="0.0" height="0.0"/>
                                                        <size key="footerReferenceSize" width="0.0" height="0.0"/>
                                                        <inset key="sectionInset" minX="10" minY="0.0" maxX="10" maxY="0.0"/>
                                                    </collectionViewFlowLayout>
                                                    <cells>
                                                        <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="CollectionViewCell" id="g9z-R1-8XJ" customClass="CollectionViewCell" customModule="stackoverflow_31582378" customModuleProvider="target">
                                                            <rect key="frame" x="10" y="2" width="180" height="180"/>
                                                            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                                            <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
                                                                <rect key="frame" x="0.0" y="0.0" width="180" height="180"/>
                                                                <autoresizingMask key="autoresizingMask"/>
                                                                <subviews>
                                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rHM-Xn-vBW">
                                                                        <rect key="frame" x="69" y="80" width="42" height="21"/>
                                                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                                                        <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                                                                        <nil key="highlightedColor"/>
                                                                    </label>
                                                                </subviews>
                                                            </view>
                                                            <color key="backgroundColor" red="0.28627450980000002" green="0.56470588239999997" blue="0.8862745098" alpha="1" colorSpace="calibratedRGB"/>
                                                            <constraints>
                                                                <constraint firstItem="rHM-Xn-vBW" firstAttribute="centerX" secondItem="g9z-R1-8XJ" secondAttribute="centerX" id="AXf-f9-ruf"/>
                                                                <constraint firstItem="rHM-Xn-vBW" firstAttribute="centerY" secondItem="g9z-R1-8XJ" secondAttribute="centerY" id="gw4-Iv-7ML"/>
                                                            </constraints>
                                                            <size key="customSize" width="180" height="180"/>
                                                            <connections>
                                                                <outlet property="label" destination="rHM-Xn-vBW" id="9SL-Kv-ZtD"/>
                                                            </connections>
                                                        </collectionViewCell>
                                                    </cells>
                                                </collectionView>
                                            </subviews>
                                            <constraints>
                                                <constraint firstItem="yY4-ue-1HX" firstAttribute="bottom" secondItem="mcy-FO-bcc" secondAttribute="bottomMargin" id="04L-lF-Idy"/>
                                                <constraint firstItem="yY4-ue-1HX" firstAttribute="leading" secondItem="mcy-FO-bcc" secondAttribute="leadingMargin" id="Fjd-8j-qvK"/>
                                                <constraint firstItem="yY4-ue-1HX" firstAttribute="trailing" secondItem="mcy-FO-bcc" secondAttribute="trailingMargin" id="PUa-ze-U5s"/>
                                                <constraint firstItem="yY4-ue-1HX" firstAttribute="top" secondItem="mcy-FO-bcc" secondAttribute="topMargin" id="XX6-d1-Vgx"/>
                                            </constraints>
                                        </tableViewCellContentView>
                                        <connections>
                                            <outlet property="collectionView" destination="yY4-ue-1HX" id="tLL-Om-JIX"/>
                                            <outlet property="collectionViewFlowLayout" destination="pPl-9q-MGc" id="Ftw-AT-QvP"/>
                                        </connections>
                                    </tableViewCell>
                                </prototypes>
                            </tableView>
                        </subviews>
                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstItem="pS5-CW-ipl" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="3vT-w2-JGU"/>
                            <constraint firstItem="pS5-CW-ipl" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="eS2-Y5-fxg"/>
                            <constraint firstItem="pS5-CW-ipl" firstAttribute="bottom" secondItem="wfy-db-euE" secondAttribute="top" id="hFA-oB-bWJ"/>
                            <constraint firstAttribute="trailing" secondItem="pS5-CW-ipl" secondAttribute="trailing" id="yin-cp-cAP"/>
                        </constraints>
                    </view>
                    <connections>
                        <outlet property="tableView" destination="pS5-CW-ipl" id="Gfe-HE-Ub6"/>
                    </connections>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="136.80000000000001" y="137.18140929535232"/>
        </scene>
    </scenes>
</document>

Results

enter image description here enter image description here


Create a usual UITableView and in your UITableViewCell create the UICollectionView. Your collectionView delegate and datasource should conform to that UITableViewCell.

Just go through this

In your ViewController

// Global Variable
var tableView: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

tableView = UITableView(frame: self.view.bounds)
    tableView.delegate = self
    tableView.dataSource = self
    self.view.addSubview(tableView)

    tableView.registerClass(TableViewCell.self, forCellReuseIdentifier: "TableViewCell")
    tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "NormalCell")
}

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 5
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    if indexPath.row == 3 {
        var cell: TableViewCell = tableView.dequeueReusableCellWithIdentifier("TableViewCell", forIndexPath: indexPath) as! TableViewCell
        cell.backgroundColor = UIColor.groupTableViewBackgroundColor()
        return cell

    } else {
        var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("NormalCell", forIndexPath: indexPath) as! UITableViewCell
        cell.textLabel?.text = "cell: \(indexPath.row)"

        return cell
    }
}

As you can see I've created two different cells, a custom TableViewCell which is returned only when the row index is 3 and a basic UITableViewCell in other indices.

The custom "TableViewCell" will have our UICollectionView. So Create a UITableViewCell subclass and write down the below code.

import UIKit

class TableViewCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegate {

var collectionView: UICollectionView!

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = UICollectionViewScrollDirection.Horizontal

    collectionView = UICollectionView(frame: self.bounds, collectionViewLayout: layout)
    collectionView.delegate = self
    collectionView.dataSource = self
    collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
    collectionView.backgroundColor = UIColor.clearColor()

    self.addSubview(collectionView)
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

// MARK: UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
    return 1
}


func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 10
}

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell: UICollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as! UICollectionViewCell
    if indexPath.row%2 == 0 {
        cell.backgroundColor = UIColor.redColor()
    } else {
        cell.backgroundColor = UIColor.yellowColor()
    }

    return cell
}
}

Hope it helps.


Think about MVC and embedding never programmatically like this above. A subclass of UIView (-> thats what a cell really is) is never the delegate class for a delegate and datasource of an tableView (same for collectionView).

Did you ever create a subclass of tableview to do all the programmatically stuff in this subclass - No! you do it in your viewcontroller - because you create UIView's in Controllers, to "control" them. So the right way you have to do is:

Let me give ya a example (on the oldschool "better-understanding" way):

  1. Create a ViewController with a tableView and add (addSubview) this collectionView to your UITableViewCell.
  2. You have an ViewController on Storyboard and also a UITableView is embedded
  3. It's also connected with your ViewController class as an Outlet (and also his delegate and datasource)
  4. Instead of adding a CollectionView now on your Custom UITableViewCell, just add a "content-holder" UIView (with constraints). Later you will use this view to add a collectionview as an subview.

  5. Now Create a New UIViewController (New File > ...) with XIB OR drag and drop a new UIViewController from properties panel in your storyboard and create also a UIViewController class. Don't forget to connect each other. (we will do the first one for better understanding)

  6. The new ViewController just handle like a ViewController with a CollectionView (same like 1. but collectionView)
  7. In this new ViewController with collectionView you handle everyhting like usual, with delegate and datasource and so on...

  8. NOW: On the ViewController (first one) with tableView you instantiate the new ViewController (with collectionView) on every cell (cellForRowAtIndexPath), and add his collectionView as a subview to the current View you created (as on content holder), e.g.:

let myViewControllerWithCollectionView = MyViewControllerWithCollectionView() myCell.contentHolderView.addSubview(myViewControllerWithCollectionView.collectionView)

What you can also do (and maybe the newer and better way, never tried myself but I'm sure it will work very well, is: UIContainerView).

Thats it! some tips for you:

  • be careful on adding a new subview in cellForRowAtIndexPath, check always if the contentHolderView has already a

    myViewControllerWithCollectionView.collectionView

to get Actions back from the collectionView, to your current View add a custom protocol (delegate) to your view, for more information. Never set delegate and datasource from your collectionView to your main tableViewController, just let handle everything on the right viewcontroller und push information to any other viewcontroller if needed.