UITableView inside UITableViewCell with dynamic height - ios

I need to put UITableView inside UITableViewCell with auto-layout because second table has different number of rows and one row can have different height.
This is my ViewController
class ViewController: UIViewController {
let tableView = UITableView()
let cellId = "firstTableCellId"
override func viewDidLoad() {
super.viewDidLoad()
setupView()
tableView.reloadData()
view.backgroundColor = UIColor.gray
}
func setupView() {
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.register(NextTable.self, forCellReuseIdentifier: cellId)
tableView.backgroundColor = UIColor.green
tableView.separatorStyle = .singleLine
view.addSubview(tableView)
view.addConstraintsWithFormat("V:|-60-[v0]-5-|", views: tableView)
view.addConstraintsWithFormat("H:|-8-[v0]-8-|", views: tableView)
}
}
extension ViewController: UITableViewDelegate {
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! NextTable
cell.layoutIfNeeded()
return cell
}
}
And NextTable which is the cell in first table
class NextTable: UITableViewCell {
var myTableView: UITableView!
let cellId = "nextTableCellId"
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.brown
setupView()
myTableView.reloadData()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.text = "Next table:"
label.textColor = UIColor.black
label.sizeToFit()
label.backgroundColor = UIColor.cyan
return label
}()
func setupView() {
myTableView = UITableView()
myTableView.delegate = self
myTableView.dataSource = self
myTableView.separatorStyle = .singleLineEtched
myTableView.backgroundColor = UIColor.blue
myTableView.register(TableCell.self, forCellReuseIdentifier: cellId)
myTableView.isScrollEnabled = false
addSubview(myTableView)
addSubview(label)
addConstraintsWithFormat("H:|-30-[v0]-30-|", views: myTableView)
addConstraintsWithFormat("H:|-30-[v0]-30-|", views: label)
addConstraint(NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 15))
addConstraint(NSLayoutConstraint(item: myTableView, attribute: .top, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1.0, constant: 0))
addConstraint(NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: myTableView, attribute: .top, multiplier: 1.0, constant: 0))
addConstraint(NSLayoutConstraint(item: myTableView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -15))
}
}
extension NextTable: UITableViewDelegate {
}
extension NextTable: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! TableCell
cell.layoutIfNeeded()
return cell
}
}
And cell in second table
class TableCell: UITableViewCell {
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.text = "Some text"
label.textColor = UIColor.black
label.sizeToFit()
label.backgroundColor = UIColor.red
return label
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.yellow
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView(){
addSubview(label)
addConstraintsWithFormat("V:|-8-[v0]-8-|", views: label)
addConstraintsWithFormat("H:|-5-[v0]-5-|", views: label)
}
}
This is the effect of my code
There is no second table so I create new class and use it in cell as new table view
class InnerTableView: UITableView {
override var intrinsicContentSize: CGSize {
return self.contentSize
}
}
And now table is show but size is too large
What can I do to show full second table without empty space at bottom of cell.

I added override for property in my table view:
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
Full code for my table in table can you find on my GitHub

In your first table view, add this to your cellForRowAt after you dequeue your cell (might need to be slightly tweaked to fit your implementation):
cell.tableView.reloadData()
DispatchQueue.main.async {
cell.tableView.scrollToRow(at: IndexPath(row: cell.numberOfRowsInInnerTableView.count.count - 1, section: 0), at: .bottom, animated: false)
}
DispatchQueue.main.async {
cell.tableView.invalidateIntrinsicContentSize()
cell.tableView.layoutIfNeeded()
self.updateHeight()
}
Then define a function updateHeight in the same class (outer table view):
func updateHeight() {
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
Obviously, this is kind of hacky, but essentially it allows the cell's InnerTableView to know the actual height of all of the inner cells and resize the outer tableview appropriately. This method worked for me.

I tried the above solution but it haven't worked for me. I have done the small change and then it worked fine for me.
A) In my ViewController:
//NOTE: TestQuestionAnsOptionCell is the outer cell
if let cell = tableView.dequeueReusableCell(withIdentifier: String(describing:TestQuestionAnsOptionCell.self), for: indexPath) as? TestQuestionAnsOptionCell {
cell.reloadData()
setUpInnerCellListner(cell: cell)
cell.selectionStyle = .none
return cell
}
private func setUpInnerCellListner(cell:TestQuestionAnsOptionCell) {
cell.reloadTable = { // Closure called from TestQuestionAnsOptionCell
DispatchQueue.main.async {
UIView.setAnimationsEnabled(false)
self.tbQuestion.beginUpdates()
self.tbQuestion.endUpdates()
UIView.setAnimationsEnabled(true)
}
}
}
Extention used here:
extension NSObject {
class var className: String {
return String(describing: self)
}
}
B) In TestQuestionAnsOptionCell:
class TestQuestionAnsOptionCell: UITableViewCell {
var tblOptions: OwnTableView = OwnTableView()
var reloadTable:(()->(Void))?
override func awakeFromNib() {
super.awakeFromNib()
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupView() {
tblOptions.estimatedRowHeight = 60.0
tblOptions.rowHeight = UITableView.automaticDimension
tblOptions.delegate = self
tblOptions.dataSource = self
tblOptions.separatorStyle = .none
tblOptions.register(UINib(nibName: TestQuestionOptionCell.className, bundle: nil), forCellReuseIdentifier: TestQuestionOptionCell.className)
tblOptions.isScrollEnabled = false
addSubview(tblOptions)
addConstraintsWithFormat("H:|-30-[v0]-30-|", views: tblOptions)
addConstraint(NSLayoutConstraint(item: tblOptions, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 15))
addConstraint(NSLayoutConstraint(item: tblOptions, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -15))
}
func reloadData() {
tblOptions.reloadData()
}
}
With TableView Delegate and datasource, I used the following:
extension TestQuestionAnsOptionCell:UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//NOTE: TestQuestionOptionCell is the inner cell
if let cell = tableView.dequeueReusableCell(withIdentifier: TestQuestionOptionCell.className, for: indexPath) as? TestQuestionOptionCell {
cell.updateCell()
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
tblOptions.invalidateIntrinsicContentSize()
tblOptions.layoutIfNeeded()
reloadTable?()//Closure to update outer tableview
}
}
C) Inner tableview is used as:
class OwnTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet{
self.invalidateIntrinsicContentSize()
}
}
}
D) UIView Extention is as:
extension UIView {
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String:UIView]()
for(index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: viewsDictionary ))
}
}

All you need is invalidate content size in side ParentTableView's cellForRowAt indexPath
parentCell.InsideTableview.invalidateIntrinsicContentSize()

I am posting another answer and I wish to make your life easier this time.
I have searched and tried everything I found on google. What worked for me was combination of what I found here and there + something weird I tried and worked:
I cannot believe that this worked but it did guys.
1- Override the UITableView as mentioned in the answers to this question + override reloadData():
class InnerAutoTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet{
self.invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
}
2- Set rowHeight to be automaticDimension and estimatedRowHeight to be 0:
private lazy var tableView: UITableView = {
let tv = InnerAutoTableView()
tv.register(ItemTableCell.self, forCellReuseIdentifier: "itemId")
tv.delegate = self
tv.dataSource = self
tv.isScrollEnabled = false
tv.rowHeight = UITableView.automaticDimension
tv.estimatedRowHeight = 0
return tv
}()
I tried this on iOS 15.4 simulator and real devices (iPhone 12 Pro Max + iPhone 12).

Related

UITableView didSelect issue with nested UiCollectionView

I have a tableView with cells that contain UiCollectionView. My didSelect tableView's delegate isn't called when I touch the cell on the collectionView.
I think it's my collectionView that get the touch instead. Do you have any elegant solution to keep the scroll enabled on my collectionView but disable the selection and pass it to the tableview ?
Here is my tableView declaration :
private lazy var tableView:UITableView = { [weak self] in
$0.register(TestTableViewCell.self, forCellReuseIdentifier: "identifier")
$0.delegate = self
$0.dataSource = self
return $0
}(UITableView())
Here are my delegate and dataSource methods:
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! TestTableViewCell
return cell
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
print(indexPath)
}
And here is my cell :
public class TestTableViewCell : UITableViewCell {
private lazy var collectionViewFlowLayout:UICollectionViewFlowLayout = {
$0.scrollDirection = .horizontal
$0.minimumLineSpacing = 0
$0.minimumInteritemSpacing = 0
return $0
}(UICollectionViewFlowLayout())
private lazy var collectionView:UICollectionView = {
$0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "identifier")
$0.delegate = self
$0.dataSource = self
return $0
}(UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout))
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: cell:"identifier", for: indexPath) as! cell:UICollectionViewCell
return cell
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.frame.size
}
}
If you spot any compilation error, excuse me, this is an anonymized copy/past. My app is running without error.
If you want an example of what I'm trying to do you can check AirBnb's app. TableView with some houses with cells and inside, pictures collectionView. Il you touch the collectionView, the tableView select the cell...
Thanks
I don't believe there is a "direct" way to do what you're asking. The collection view will respond to the gestures, so they can't "flow through" to the table view / cells without using a closure or protocol/delegate pattern.
Here's one approach...
We subclass UICollectionView and, on touchesBegan call a closure to tell the table view to select the row.
We can also implement scrollViewWillBeginDragging to select the row on collection view scroll, in addition to collection view "tap."
So...
View Controller class
class TableColTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// number of items in the collection view for each row
let myData: [Int] = [
12, 15, 8, 21, 17, 14,
16, 10, 5, 13, 20, 19,
]
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
tableView.register(SomeTableCell.self, forCellReuseIdentifier: "someTableCell")
tableView.dataSource = self
tableView.delegate = self
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "someTableCell", for: indexPath) as! SomeTableCell
cell.rowTitleLabel.text = "Row \(indexPath.row)"
cell.numItems = myData[indexPath.row]
// closure for the cell to tell us to select it
// because its collection view was tapped
cell.passThroughSelect = { [weak self] theCell in
guard let self = self,
let pth = self.tableView.indexPath(for: theCell)
else { return }
self.tableView.selectRow(at: pth, animated: true, scrollPosition: .none)
// selecting a row programmatically does NOT trigger didSelectRowAt
// so we'll call our own did select func
self.myDidSelect(pth)
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// pass this on to our own did select func
// so it matches what happens when programmatically selecting the row
myDidSelect(indexPath)
}
func myDidSelect(_ indexPath: IndexPath) {
print("Table View - didSelectRowAt", indexPath)
// do something because the row was selected
}
}
Table View Cell class
class SomeTableCell: UITableViewCell {
// callback closure
var passThroughSelect: ((UITableViewCell) -> ())?
var rowTitleLabel = UILabel()
var collectionView: SubCollectionView!
var numItems: Int = 0 {
didSet {
collectionView.reloadData()
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .horizontal
fl.estimatedItemSize = CGSize(width: 120.0, height: 52.0)
fl.minimumLineSpacing = 12
fl.minimumInteritemSpacing = 12
collectionView = SubCollectionView(frame: .zero, collectionViewLayout: fl)
rowTitleLabel.translatesAutoresizingMaskIntoConstraints = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(rowTitleLabel)
contentView.addSubview(collectionView)
let g = contentView.layoutMarginsGuide
// avoid auto-layout complaints
let c = collectionView.heightAnchor.constraint(equalToConstant: 60.0)
c.priority = .required - 1
NSLayoutConstraint.activate([
rowTitleLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
rowTitleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
rowTitleLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
rowTitleLabel.heightAnchor.constraint(equalToConstant: 30.0),
collectionView.topAnchor.constraint(equalTo: rowTitleLabel.bottomAnchor, constant: 4.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
c,
])
collectionView.register(SomeCollectionCell.self, forCellWithReuseIdentifier: "someCollectionCell")
collectionView.dataSource = self
collectionView.delegate = self
collectionView.passThroughTouch = { [weak self] in
guard let self = self else { return }
self.passThroughSelect?(self)
}
collectionView.backgroundColor = .systemYellow
}
}
extension SomeTableCell: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numItems
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "someCollectionCell", for: indexPath) as! SomeCollectionCell
cell.label.text = "Cell \(indexPath.item)"
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Collection View - didSelectItemAt", indexPath)
}
}
extension SomeTableCell: UIScrollViewDelegate {
// if you only want the table cell selected on TAP, don't implement this
// if you want the table cell selected on CollectionView SCROLL, implement this
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.passThroughSelect?(self)
}
}
UICollectionView subclass
class SubCollectionView: UICollectionView {
var passThroughTouch: (() -> ())?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// this allows the collection view cell to be selected
super.touchesBegan(touches, with: event)
// this tells the controller (the table view cell)
// that the collection view was tapped
passThroughTouch?()
}
}
UICollectionView Cell class
class SomeCollectionCell: UICollectionViewCell {
var label = UILabel()
var styleView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
styleView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
styleView.addSubview(label)
contentView.addSubview(styleView)
let g = contentView
NSLayoutConstraint.activate([
styleView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
styleView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
styleView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
styleView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
label.topAnchor.constraint(equalTo: styleView.topAnchor, constant: 8.0),
label.leadingAnchor.constraint(equalTo: styleView.leadingAnchor, constant: 8.0),
label.trailingAnchor.constraint(equalTo: styleView.trailingAnchor, constant: -8.0),
label.bottomAnchor.constraint(equalTo: styleView.bottomAnchor, constant: -8.0),
])
label.textColor = .white
styleView.backgroundColor = UIColor(white: 0.5, alpha: 1.0)
styleView.layer.borderColor = UIColor.white.cgColor
styleView.layer.borderWidth = 1
styleView.layer.cornerRadius = 6
}
override var isSelected: Bool {
didSet {
label.textColor = isSelected ? .red : .white
styleView.backgroundColor = isSelected ? UIColor(white: 0.95, alpha: 1.0) : UIColor(white: 0.5, alpha: 1.0)
styleView.layer.borderColor = isSelected ? UIColor.red.cgColor : UIColor.white.cgColor
}
}
}
You can do the following
First in first, print something in didSelectItem of your collectionView delegate, to make ensure that your collectionView cell is tapped.
add a delegate property in your UICollectionViewClass and call the delegate in the DidSelectItem if the step 1 is performing correctly.
In your UITableViewController, you have a function cellForRowAtIndexPath, here add the delegate property for the associate cell.
If you can print something in your delegate function, then you are at your last step. Call super.didSelect..with your indexPath, because now you have everything to call didSelect manually.
Maybe you would like to try allowsSelection property of UICollectionView. If you set this property false, your collection view cells are not selected any more.
And I think tableView didSelect can capture user interaction. If UITableView still can not capture didSelect, you can give delegate to collectionView and fire it once it's tapped by adding a tap gesture onto UICollectionView.

Swift: height of tableView row whose tableView cell has nested tableView with dynamic number of rows

I have been looking around for a solution or a best way to determine the height of a tableView row in heightForRowAt, that has a tableView based on some conditions in the data model.
When my data model has a data type called MULTISELECT, I need to display a cell with a tableView inside it. There are no problems in doing so. The inner tableView's data is assigned in outer tableView's cellForRowAt.
The question here is how to get the height of my outer tableView row for the MULTISELECT type cells, after the data is populated for the inner tableView rows?
Outer tableView code (inside a ViewController) -
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let preferenceCategories = self.preferenceCategories else {
return UITableViewCell()
}
let categoryCode = preferenceCategories[indexPath.section].code
let filteredPreferenceSet = self.preferenceSet.filter({$0.categoryCode == categoryCode}).filter({$0.dataType == "BOOLEAN"/* || $0.dataType == "MULTISELECT"*/})
if let preferenceDataType = filteredPreferenceSet[indexPath.row].dataType {
if preferenceDataType == "BOOLEAN" {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "CustPrefSetCell", for: indexPath) as! CustPrefSetCell
cell.preferenceName.text = filteredPreferenceSet[indexPath.row].name
cell.preferenceDescription.text = filteredPreferenceSet[indexPath.row].description
cell.switchDelegate = self
let propertyValue = ((filteredPreferenceSet[indexPath.row].value ?? "false") as NSString).boolValue
propertyValue ? cell.preferenceSwitch.setOn(true, animated: true) : cell.preferenceSwitch.setOn(false, animated: true)
cell.preferenceCode = filteredPreferenceSet[indexPath.row].code
return cell
}
else if preferenceDataType == "MULTISELECT" {
let multiSelectCell = self.tableView.dequeueReusableCell(withIdentifier: "CustPrefMultiSelectTableViewCell", for: indexPath) as! CustPrefMultiSelectTableViewCell
multiSelectCell.preferenceValues = filteredPreferenceSet[indexPath.row].preferenceValues
// self.rowHeight = multiSelectCell.tableView.contentSize.height
return multiSelectCell
}
else {
return UITableViewCell()
}
}
else {
return UITableViewCell()
}
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
The inner tableView is inside the multiSelectCell, whose code is below -
class CustPrefMultiSelectTableViewCell: UITableViewCell {
#IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var preferenceDescription: UILabel!
#IBOutlet weak var preferenceTitle: UILabel!
#IBOutlet weak var tableView: UITableView!
var preferenceValues: [PreferenceValue]?
override func awakeFromNib() {
super.awakeFromNib()
self.tableView.delegate = self
self.tableView.dataSource = self
guard let frameworkBundle = Bundle(identifier: "com.frameworkbundle.asdf") else {
fatalError("Framework bundle identifier is incorrect.")
}
let custPrefHeaderCell = UINib(nibName: "CustPrefMultiSelectPreferenceTableViewCell", bundle: frameworkBundle)
self.tableView.register(custPrefHeaderCell, forCellReuseIdentifier: "CustPrefMultiSelectPreferenceTableViewCell")
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = 64.0
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension CustPrefMultiSelectTableViewCell: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let preferenceValues = self.preferenceValues else {
return 0
}
return preferenceValues.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let preferenceCategories = self.preferenceValues else {
return UITableViewCell()
}
let cell = self.tableView.dequeueReusableCell(withIdentifier: "CustPrefMultiSelectPreferenceTableViewCell", for: indexPath) as! CustPrefMultiSelectPreferenceTableViewCell
cell.preferenceName.text = preferenceCategories[indexPath.row].name
cell.preferenceDescription.text = preferenceCategories[indexPath.row].description
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
I thought of an approach by having a height constraint for the inner tableView, and update the outer tableView height when it is ready/reloaded with data. But where should I implement that logic? With a fixed height of inner tableView, I get an unwanted behavior of scrolling. That need to be avoided.
How do I go further with this?
Thanks in advance!
I think using nested tableView is not the best solution, anyway, I hope this example will help you.
struct Foo {
let strings: [String]
}
class NestedViewController: UIViewController {
let dataSource = [Foo(strings: ["String1", "String2"]),
Foo(strings: ["Long long long long long long long long long long long long long string"])]
let tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(NestedCell.self, forCellReuseIdentifier: NestedCell.identifier)
tableView.tableFooterView = UIView()
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
setupConstraints()
tableView.dataSource = self
tableView.delegate = self
tableView.reloadData()
}
func setupConstraints() {
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
extension NestedViewController: UITableViewDelegate & UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: NestedCell.identifier, for: indexPath) as? NestedCell else {
return UITableViewCell()
}
cell.setup(foo: dataSource[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
NestedCell.heightFor(foo: dataSource[indexPath.row])
}
}
class NestedCell: UITableViewCell {
static let identifier = "NestedCell"
let nestedTableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(TextCell.self, forCellReuseIdentifier: TextCell.identifier)
tableView.tableFooterView = UIView()
return tableView
}()
private var foo = Foo(strings: [""])
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(nestedTableView)
setConstraints()
nestedTableView.dataSource = self
nestedTableView.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(foo: Foo) {
self.foo = foo
nestedTableView.reloadData()
}
static func heightFor(foo: Foo) -> CGFloat {
foo.strings.reduce(0) { $0 + TextCell.heightFor(text: $1) }
}
private func setConstraints() {
NSLayoutConstraint.activate([
nestedTableView.topAnchor.constraint(equalTo: contentView.topAnchor),
nestedTableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
nestedTableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
nestedTableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])
}
}
extension NestedCell: UITableViewDelegate & UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
foo.strings.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TextCell.identifier, for: indexPath) as? TextCell else {
return UITableViewCell()
}
cell.setup(text: foo.strings[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
TextCell.heightFor(text: foo.strings[indexPath.row])
}
}
class TextCell: UITableViewCell {
static let identifier = "TextCell"
static let labelOffset: CGFloat = 10
private let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.font = .systemFont(ofSize: 15, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
setConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(text: String) {
label.text = text
}
static func heightFor(text: String) -> CGFloat {
text.height(width: UIScreen.main.bounds.width - 2 * TextCell.labelOffset,
font: .systemFont(ofSize: 15, weight: .medium)) + 2 * TextCell.labelOffset
}
private func setConstraints() {
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TextCell.labelOffset),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -TextCell.labelOffset),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: TextCell.labelOffset),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -TextCell.labelOffset)
])
}
}
extension String {
func height(width: CGFloat, font: UIFont) -> CGFloat {
let rect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
return ceil(boundingBox.height)
}
}

Indent for a title in a table

I have a table with two sections. Property textAligment = .right for header sections. But it's too close to the border of the phone. How to indent from the border closer to the center?
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let header = view as! UITableViewHeaderFooterView
header.textLabel?.textColor = .black
header.textLabel?.textAlignment = .right
}
Here's a very simple example...
Add a UITableViewController in Storyboard.
Assign its Custom Class to SectionTableViewController (from the code below).
Use the default cell.
Give the cell an Identifier of "DefCell"
Run the app. This is what you should see:
I've constrained the label to use contentView.layoutMarginsGuide and set the trailing constant to -32
class MySectionHeaderView: UITableViewHeaderFooterView {
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textColor = .black
v.textAlignment = .right
// during development, give it a background color to make it easy to see the frame
//v.backgroundColor = .cyan
return v
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(myLabel)
// use layout margins guide
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
myLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
myLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
// set the constant to provide more trailing space as desired
// Note: must be a negative value
myLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -32.0),
])
}
}
class SectionTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: "MyHeader")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "DefCell", for: indexPath)
c.textLabel?.text = "Section: \(indexPath.section) Row: \(indexPath.row)"
return c
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: "MyHeader") as? MySectionHeaderView else {
fatalError("Could not dequeue MySectionHeaderView!")
}
v.myLabel.text = "Section \(section)"
return v
}
}
You'll notice these lines in the declaration of myLabel in MySectionHeaderView:
// during development, give it a background color to make it easy to see the frame
//v.backgroundColor = .cyan
This is how it looks when the label has a background color, so you can easily see how the label is laid-out:

Select collection view cell on load - Custom UICollectionView class

I have a custom collection view (tabBarCollectionView), which should load with the first cell being selected, as per the code below. However, this is not working. I know this as the cell should be a different colour when selected, but isn't.
override init(frame: CGRect) {
super.init(frame: frame)
tabBarCollectionView.register(TabBarCell.self, forCellWithReuseIdentifier: cellIdentifier)
addSubview(tabBarCollectionView)
tabBarCollectionView.translatesAutoresizingMaskIntoConstraints = false
addConstraintsWithFormat("H:|[v0]|", views: tabBarCollectionView)
addConstraintsWithFormat("V:|[v0]|", views: tabBarCollectionView)
tabBarCollectionView.selectItem(at: IndexPath(item: 0, section: 0), animated: false, scrollPosition: [])
}
Full code of custom UIView class is inserted below:
import Foundation
import UIKit
class TabBar: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
let cellIdentifier = "Cell"
let imageNames = ["homeIcon", "exploreIcon", "addIcon", "inboxIcon", "profileIcon"]
lazy var tabBarCollectionView: UICollectionView = {
// All collection view implementations in here
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.backgroundColor = UIColor.clear
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
tabBarCollectionView.register(TabBarCell.self, forCellWithReuseIdentifier: cellIdentifier)
addSubview(tabBarCollectionView)
tabBarCollectionView.translatesAutoresizingMaskIntoConstraints = false
addConstraintsWithFormat("H:|[v0]|", views: tabBarCollectionView)
addConstraintsWithFormat("V:|[v0]|", views: tabBarCollectionView)
self.tabBarCollectionView.allowsSelection = true
self.tabBarCollectionView.selectItem(at: IndexPath(item: 0, section: 0), animated: false, scrollPosition: [])
}
override func layoutSubviews() {
super.layoutSubviews()
tabBarCollectionView.allowsSelection = true
tabBarCollectionView.selectItem(at: IndexPath(item: 0, section: 0), animated: false, scrollPosition: [])
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! TabBarCell
cell.backgroundColor = UIColor.clear
cell.tabBarImageView.image = UIImage(named: imageNames[indexPath.item])?.withRenderingMode(.alwaysTemplate)
cell.tabBarImageView.tintColor = UIColor.pinpointGrey
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width / 5, height: frame.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TabBarCell: UICollectionViewCell {
let tabBarImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "homeIcon")?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = UIColor.pinpointGrey
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
backgroundColor = UIColor.clear
addSubview(tabBarImageView)
addConstraintsWithFormat("H:[v0(42)]", views: tabBarImageView)
addConstraintsWithFormat("V:[v0(42)]", views: tabBarImageView)
addConstraint(NSLayoutConstraint(item: tabBarImageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0))
addConstraint(NSLayoutConstraint(item: tabBarImageView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0))
}
override var isHighlighted: Bool {
didSet {
if tabBarImageView.isHighlighted == true {
self.tabBarImageView.tintColor = UIColor.pinpointBlue
} else {
self.tabBarImageView.tintColor = UIColor.pinpointGrey
}
}
}
override var isSelected: Bool {
didSet {
tabBarImageView.tintColor = isSelected ? UIColor.pinpointBlue : UIColor.pinpointGrey
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Where is your datasource set? Do you load it separately and then do a reloadData()?
If yes, then you should select the cell then and not during your viewDidLoad method as during load the collection view might not have been populated yet.
You could also add to a viewWillAppear/viewDidAppear method as this gets called after the collection view has been populated

Swift UICollectionView in a UITableViewCell dynamic layout can't change cell height

In Xcode 8.2.1 and Swift 3 I have a UICollectionView inside a UITableViewCell but I can't get the layout right. basically I want to be able to see all of the star. I can adjust the size of the star but I want to fix the underlying problem, expanding the blue band.
Here is a shot of the view The red band is contentView and the blue band is collectionView. At first glance it looks like collectionView height is the problem but it is actually being hidden by something else. I can't find or change whatever it is hiding the collectionView cell.
I am not using the storyboard beyond a view controller embedded in a navigator.
Here's all the relevant code.
class WishListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView = UITableView()
let tableCellId = "WishListsCell"
let collectionCellId = "WishListsCollectionViewCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .white
tableView.register(WishListsCell.self, forCellReuseIdentifier: tableCellId)
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
func numberOfSections(in tableView: UITableView) -> Int {
return wishLists.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCell(withIdentifier: tableCellId, for: indexPath) as! WishListsCell
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView,willDisplay cell: UITableViewCell,forRowAt indexPath: IndexPath) {
guard let tableViewCell = cell as? WishListsCell else { return }
//here setting the uitableview cell contains collectionview delgate conform to viewcontroller
tableViewCell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forSection: indexPath.section)
tableViewCell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
}
func tableView(_ tableView: UITableView,didEndDisplaying cell: UITableViewCell,forRowAt indexPath: IndexPath) {
guard let tableViewCell = cell as? WishListsCell else { return }
storedOffsets[indexPath.row] = tableViewCell.collectionViewOffset
}
}
extension WishListsViewController: UICollectionViewDelegate , UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionCellId, for: indexPath) as! WishListsCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 100)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
}
class WishListsCell: UITableViewCell {
private var collectionView: UICollectionView!
let cellId = "WishListsCollectionViewCell"
var collectionViewOffset: CGFloat {
get { return collectionView.contentOffset.x }
set { collectionView.contentOffset.x = newValue }
}
func setCollectionViewDataSourceDelegate<D: protocol<UICollectionViewDataSource, UICollectionViewDelegate>>
(dataSourceDelegate: D, forSection section : Int) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = section
collectionView.reloadData()
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
collectionView = UICollectionView(frame: contentView.frame, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .blue
collectionView.register(WishListsCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
contentView.backgroundColor = .red
self.contentView.addSubview(collectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class WishListsCollectionViewCell: UICollectionViewCell {
var imageView: UIImageView
override init(frame: CGRect) {
imageView = UIImageView()
super.init(frame: frame)
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
contentView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
NSLayoutConstraint.activate([
NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal,
toItem: contentView, attribute: .width,
multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal,
toItem: contentView, attribute: .height,
multiplier: 1.0, constant: 0.0),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You can Use View Debugging
Run the project, go back to Xcode and click on the Debug View Hierarchy button in the Debug bar. Alternatively, go to Debug\View Debugging\Capture View Hierarchy.
Now you can select your red view and Xcode will indicate which view is selected.
For more info. Check out: View Debugging in Xcode
Specifically, the problem was the collection view frame had not been properly set. This was only revealed by the great tip that I didn't know about in the accepted answer.
This fixed it:
self.collectionView.frame = CGRect(0, 0, screenWidth, (screenWidth / 4) * Constants.IMAGE_ASPECT_RATIO)

Resources