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
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):
- Create a ViewController with a tableView and add (addSubview) this collectionView to your UITableViewCell.
- You have an ViewController on Storyboard and also a UITableView is embedded
- It's also connected with your ViewController class as an Outlet (and also his delegate and datasource)
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.
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)
- The new ViewController just handle like a ViewController with a CollectionView (same like 1. but collectionView)
In this new ViewController with collectionView you handle everyhting like usual, with delegate and datasource and so on...
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.