The Issue
I am attempting to use a collection view in a view controller for cards. When a user taps on a card, it expands. At all times, I have a tableview in each card, whether it is expanded or not. I have the data loading in the table views, but only when I tap on a card to expand it or if I scroll collection view cards offscreen and back onscreen. What is a cleaner workflow to doing this that puts tableviews in each collection view cell?
This is in my main view controller:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "productionBinderCell", for: indexPath) as? ProductionBinderCollectionViewCell
let suck = cell?.detailsTableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? DescriptionTableViewCell
suck?.descriptionText.text = productions[indexPath.row].productionDescription
cell?.detailsTableView.reloadData()
cell?.layer.shouldRasterize = true
cell?.layer.rasterizationScale = UIScreen.main.scale
let title = productions[indexPath.row].productionTitle
let luck = productions[indexPath.row].productionImage
let zuck = UIImage(data:luck!)
cell?.productionTitle.text = title
cell?.productionFeaturedImage.image = zuck
cell?.productionColorTint.tintColor = UIColor.blue
let backbtn = cell?.viewWithTag(1)
backbtn?.isHidden = true
return cell!
}
This is in a subclass of the tableview:
override func layoutSubviews() {
super.layoutSubviews()
self.dataSource = self
self.delegate = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "productionDescription", for: indexPath) as? DescriptionTableViewCell
return cell!
}
This is in a subclass of the tableview cell I am concerned with. It only shows the uilabel text when scrolling offscreen or tapping on the collection view cell:
class DescriptionTableViewCell: UITableViewCell {
#IBOutlet var descriptionText: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Maybe I shouldn't be using a tableview at all? Thank you very much to all who help. I am very open to criticism and love learning from my mistakes (a little too often). :)
What you need, first of all, is clear definition of views and cells.
UICollectionview is a collection of UICollectionViewCell objects
UITableView is a collection of UITableViewCell objects
Do you need to embed entire table view within each card? There you have your answer. You have parent-child relationship between your UICollectionViewCell and UITableView derived classes:
Derive from UICollectionViewCell - MyCollectionViewCell - this step is mandatory.
Inside xib for MyCollectionViewCell, insert a tableview, with horizontal scrolling enabled. (this is an option inside xib attribute editor)
If needed to customize, also derive from UITableViewCell - MyTableViewCell. This step is optional.
Inside MyCollectionViewCell class file, have implementation for table view datasource and delegate methods. That way, each card will have all its table view cell children in one place
If present, define custom logic inside MyTableViewCell class file
There is no need to subclass UITableView here. Everything that needs to be customized can be done using subclassing cells.
Related
So i have a horizontal UICollectionView of images that is inside a vertical UICollectionView and i want to detect which cell is in the center on the horizonal UICollectionView when i select a cell from the vertical one
I tried to send a notification and call a function that does the work but its called multiple times since its reusing the same cell so at the end i dont get the appropriate indexPath.
And also when i tap on an image the "didSelectItemAt" of the horizontal collectionView is called, is there a way to get the vertical one to be called instead ?
thanks
Your best bet would be delegates via protocols
Create protocol for your collection view cells
protocol yourDelegate: class {
func didSelectCell(WithIndecPath indexPath: IndexPath, InCollectionView collectionView: UICollectionView) -> Void
}
In your cells, create a function called setup which you can call at cellForRow.
In your cells, create a touch recogniser for self.
Disable cell selection for your collection views since these delegates will be called when the user touches given cell.
class yourCell: UICollectionViewCell {
var indexPath: IndexPath? = nil
var collectionView: UICollectionView? = nil
weak var delegate: yourDelegate? = nil
override func awakeFromNib() {
super.awakeFromNib()
let selfTGR = UITapGestureRecognizer(target: self, action: #selector(self.didTouchSelf))
self.contentView.addGestureRecognizer(selfTGR)
}
#objc func didTouchSelf() {
guard let collectionView = self.collectionView, let indexPath = self.indexPath else {
return
}
delegate?.didSelectCell(WithIndecPath: indexPath, InCollectionView: collectionView)
}
func setupCell(WithIndexPath indexPath: IndexPath, CollectionView collectionView: UICollectionView, Delegate delegate: yourDelegate) {
self.indexPath = indexPath
self.collectionView = collectionView
self.delegate = delegate
}
}
In your viewController, create extension for this protocol and if you do everything right, when the user touches your cell, the cell will call you via this delegation.
extension YourViewController: yourDelegate {
func didSelectCell(WithIndecPath indexPath: IndexPath, InCollectionView collectionView: UICollectionView) {
//You have your index path and you can "if" your colllection view
if collectionView == self.yourFirstCollectionView {
} else if collectionView == self.yourSecondCollectionView {
}
//and so on..
}
}
since protocol is ": class", we can use weak for your delegate property so no memory leak will occur. I use this method for tableViews and collectionViews throughout my projects.
I have an issue where the data presented in a UICollectionView overwrites the label and the cell view is not getting cleared.
This image shows the issue,
IE:
My UICollectionViewCell which is constructed like so;
// in viewDidLoad
self.playerHUDCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier:reuseIdentifer)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:UICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifer, for: indexPath) as UICollectionViewCell
let arr = UINib(nibName: "EYPlayerHUDView", bundle: nil).instantiate(withOwner: nil, options: nil)
let view = arr[0] as! EYPlayerHUDView
cell.contentView.addSubview(view)
if let allPlayers = self.allPlayers
{
let player:EYPlayer = allPlayers[indexPath.row]
view.updatePlayerHUD(player: player)
}
cell.layoutIfNeeded()
return cell
}
I use a view to display in the cell.
I tried removing all the cell's subchildren in the cellForItemAt but it appears to remove all the subviews.
I would like to know how do I clear the UICollectionViewCell so labels and other info on the UICollectionViewCell is not dirty like the example above.
Many thanks
Use prepareForReuse method in your custom cell class, something like this:
override func prepareForReuse() {
super.prepareForReuse()
//hide or reset anything you want hereafter, for example
label.isHidden = true
}
in your cellForItemAtIndexPath, instantiate your custom cell:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCellIdentifier", for: indexPath) as! CustomViewCell
Then, always in cellForItemAtIndexPath, setup your items visibility/values
//cell = UICollectionViewCell
for subview in cell.contentView.subviews {
// you can place "if" condition to remove image view, labels, etc.
//it will remove subviews of cell's content view
subview.removeFromSuperview()
}
UICollectionViewCells are reused to avoid instantiations, to optimize the performance. If you are scrolling and a cell becomes invisible, the same object is used again (dequeueReusableCell) and a new content is set in cellForItemAt...
As mentioned in the previous answers, before reusing the cell, prepareForReuse() is called on the cell. So you can overrride prepareForReuse() and do whatever preparation you need to do.
You are however creating and adding a new EYPlayerHUDView to the cell on every reuse, so your cell becomes full of stacked EYPlayerHUDViews.
To avoid this, subclass UICollectionViewCell and make the EYPlayerHUDView a property of your custom cell (I recommend to use a XIB):
class MyCell: UICollectionViewCell {
#IBOutlet var player:EYPlayerHUDView!
override func prepareForReuse() {
super.prepareForReuse()
// stop your player here
// set your label text = ""
}
}
After doing so, you can update the EYPlayerHUDView in cellForItemAt without instantiating it and without adding it as new view:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifer, for: indexPath) as? MyCell else {
return nil
}
if let allPlayers = self.allPlayers {
let player:EYPlayer = allPlayers[indexPath.row]
cell.player.updatePlayerHUD(player: player)
}
return cell
}
(Code untested)
Make custom UICollectionView class and implement prepareForReuse to clear the content if needed.
Scenario:
I have 2 VC -
ChildViewController
It has a tableView which displays a list of items. I need to pass the tableView.contentSize.height value, after the table is populated to my ParentVC. For that I am using delegate as
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableVieww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath)
cell.textLabel?.text = "heyy"
hght.constant = tableVieww.contentSize.height
if flag == true
{
delegate.tableHeight(tableVieww.contentSize.height)
print(tableVieww.contentSize.height)
flag = false
}
return cell
}
ParentViewController
It has a tableView with one cell. This cell is showing view of a childVC i.e nwVC. I want to change the cell height depending upon the height of my ChildVC's tableView.
I am adding the childVC's view by the following code & I know this is the wrong place to do so but I am not getting how,where and what to do, to get the childViewController's function to be called before the ParentViewController's functions?
vc3 = self.storyboard?.instantiateViewControllerWithIdentifier("nwVC") as? nwVC//newVC is ChildViewController
vc3!.view!.frame = cell.myview.bounds
vc3!.didMoveToParentViewController(self)
cell.myview.addSubview(vc3!.view)//UIView inside the cell
vc3!.delegate=self
Problem -
The delegate methods of ParentViewController's tableView gets called before the childViewController's function's are called for which I cannot update my rowHeight as per the childVC's table content.
Finally,I figured out something that works but still I want suggestions from iOS dev's viewing this question.
Mark: I could not perform the loading of childVC's functions before the loading of ParentVC's table view delegate functions but I did something which works quite good.
In my ParentVC's
override func viewWillAppear(animated: Bool) {
vc3 = self.storyboard?.instantiateViewControllerWithIdentifier("nwVC") as? nwVC
addChildViewController(vc3!)
vc3!.didMoveToParentViewController(self)
vc3!.delegate=self
}
//childVC's delegate function implementation
func tableHeight(height: CGFloat) {
height = height//I get the table view height from the childVC,height is a variable declared as var height = 200.0(it can be any value > 0)
print(ht)
self.tableVIeww.reloadData()//reload my tableView
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return hgt//at first call it returns the default value but in the 2nd call it returns the value sent by childVC
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableVIeww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath) as! myTVC
cell.backgroundColor = UIColor.greenColor()
vc3?.view.frame = cell.myview.bounds
cell.myview.addSubview((vc3?.view)!)//myView is the view in the cell's content view pinned to its edges.
return cell
}
Pro's & Con's
Pro's
The biggest advantage is that you get the works to be done.
Con's
As you can see that ChildVC's view is added 2 times (1 with the default cell size of height variable & the 2nd time when the table reloads). I feel that this might hamper the performance slightly & if Data is dynamic it might process for a bit long.
Please feel free to suggest...
I am working on a IOS Swift based project that uses a few classes to customize the UITableView and the UITableViewCell. Now one of my Cells inside the UITableView has an inner UITableView. I was wondering if it is possible when inside the cellForRowAtIndexPath, that I could also populate cells programmatically in that same process.
EX:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! CustomTableViewCell
...... do stuff
cell.detailview.uitableview <!-- populate the cells here?
.......
return cell
}
Suggestions?
Assuming three different types of cells:
class NormalTableViewCell: UITableViewCell: This is used for the "regular" cells of your outer (main) table view.
class TableContainingTableviewCell : UITableViewCell: This is used for the "special" cells of your outer (main) table view, that contain a table view (inner) within themselves.
class InnerTableViewCell : UITableViewCell: This is used for the cells of your inner table views (those contained in cells of class TableContainingTableviewCell).
(Replace each by your actual class names).
, you can use this code:
override func viewDidLoad()
{
super.viewDidLoad()
// This can also be done in storyboard with prototype cells:
self.tableView.registerClass(NormalTableViewCell.class, forCellReuseIdentifier: normalCellIdentifier)
self.tableView.registerClass(TableContainingTableViewCell.class, forCellReuseIdentifier: specialCellIdentifier)
}
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath
) -> UITableViewCell
{
if tableView == self.tableView {
// [A] OUTER TABLE VIEW
if indexPath == index path of table-containing cell {
// (A.1) TABLE-CONTAINING CELL
let cell = tableView.dequeueReusableCellWithIdentifier(specialCellIdentifier, forIndexPath: indexPath) as! TableContainingTableViewCell
// (...configure cell...)
// Setup and refresh inner table view:
cell.contentView.tableView.dataSource = self
// This is needed for dequeueing to succeed:
cell.contentView.tableView.registerClass(InnerTableViewCell.class, forCellReuseIdentifier: innerCellIdentifier)
cell.contentView.tableView.reloadData()
// ^ THIS TRIGGERS A CALL TO THIS FUNCTION, ON THE
// INNER TABLE VIEW (PATH [B] BELOW)
return cell
}
else {
// (A.2) NORMAL CELL
let cell = tableView.dequeueReusableCellWithIdentifier(normalCellIdentifier, forIndexPath: indexPath) as! NormalTableViewCell
// (configure cell)
return cell
}
}
else {
// [B] INNER TABLE VIEW
let cell = tableView.dequeueReusableCellWithIdentifier(innerCellIdentifier, forIndexPath: indexPath) as! InnerTableViewCell
// (configure cell)
return cell
}
}
...but I would strongly argue against having a table view embedded inside a another table view's cell. At the very least, make sure the inner table view does not need to scroll (i.e., the containing cell is high enough to show all rows and the table itself has scroll disabled).
I'm having an issue where my app will freeze if when I press it to got the next scene which is a view controller with a tableview nested inside of it. the app will only freeze when I've declared how many rows the table will have. If anyone knows how to fix this problem its be greatly appreciated.
class ViewController: UIViewController, UITableViewDataSource {
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 {
var cell = UITableViewCell()
cell.textLabel?.text = "hello"
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Have you set dataSource of your table view? You must call tableView.dataSource = self somewhere in your code (most probably in viewDidLoad method)
The problem with your code is that in tableView(_:cellForRowAtIndexPath:) you are creating new cell every time the method is called. That is not how table views (or collection views) work.
The idea behind these components is to reuse their cells so that your application shows better performance. What if you have 1000 cells? You will create 1000 UITableViewCell objects for each cell? This is where dequeueReusableCellWithIdentifier(_:) comes in handy.
So set an identifier for your cell in Interface Builder, for example "Cell" and get the cell like this:
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
This is the right way to go.
You have to make sure you set the correct reuse identifier for your table view cell (either in storyboard, or code).
Storyboard:
Of if you want to do it in code, put this in viewDidLoad():
tableView.registerClass(UITableViewCell, forHeaderFooterViewReuseIdentifier: "cellIdentifier")
Important to know that you should only use one of the above. If you define your UI in Storyboards, I recommend the first approach.
Then when you create the cell in tableView(_:, cellForRowAtIndexPath), instead of this:
var cell = UITableViewCell()
Use a cell from the reuse queue like so:
var cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath) as! UITableViewCell
Everytime you are creating object for cell, make sure you use reuse identifier.
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell