I have looked around for this issue, and have not found an answer that works for my case. Basically, I have a tableView where each cell contains a collectionView. What I would like to do is to refresh the specific TableViewCell where the collectionView was scrolled, in order to update a label under the CollectionView, within that TableViewCell.
So when the user scrolls on the collectionView, I need to know in which cell that collectionView is. I have tried using didSelectRowAtIndexPath, however, it only is called when the non-CollectionView part of the cell is tapped. When the collectionView is tapped or scrolled, it isn't called. I am coding in Swift.
Any ideas on how I can do this?
Thanks!
This seems like a architecture issue. Sure it can be done the way you want, but it'd be much easier if your rearranged some things. There is a fundamental problem how you want to do this. You want to manage all of the cells and their collection views directly from your view controller. But this poses the problem of needing to know where messages are coming from and directing messages back to the correct cells and collection views. This will create a lot of bloat that can be fixed with a simple UITabelViewCell subclass. It also is a step in contracting Massive View Controller syndrome. Instead, you should make the individual cells responsible for managing their own collection views.
First off, make the UITableViewCell own and be the delegate and data source of the UICollectionView. This centralizes the data and more closely models the tree of data that you actually see on screen.
class CollectionTableViewCell: UITableViewCell {
#IBOutlet var collectionView: UICollectionView! {
didSet(newCollectionView) {
newCollectionView.delegate = self;
newCollectionView.dataSource = self;
}
}
var model: NSArray?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
extension CollectionTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return model?.count ?? 0
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
// configure cell
return cell
}
}
Now you wanted to have a state refresh when your collection view scrolls. So we're going to add a label to the cell in the prototype cell (Nib or Storyboard.)
#IBOutlet var indicatorLabel: UILabel!
And you want to update it when the collection view is scrolled. We can do that with the scrollViewDidScroll method off the UIScrollViewDelegate protocol. Because UICollectionViewDelegate implements the UIScrollViewDelegate, it's available for us to use since we implement the UICollectionViewDelegate in our extension.
// in the CollectionTableViewCell extension...
func scrollViewDidScroll(scrollView: UIScrollView) {
let scrollViewOffset = scrollView.contentOffset.x
let scrollViewWidth = CGRectGetWidth(scrollView.frame)
let completionString = String(format: "%# / %#", scrollViewOffset, scrollViewWidth)
self.indicatorLabel.text = completionString
}
So by making the individual cells responsible for their own respected collection views, we make managing them easier. It allows us organize our code to be more compact, understandable, and keeps us from getting Massive View Controller syndrome. The more code you can move out of your view controller, the better.
Some good talks to hear on this would be:
WWDC 2014 – Advanced iOS Application Architecture and Patterns
Let's Play: Refactor the Mega Controller!
You can use tag property of UITableViewCell. Set tag to row number and when cell is tapped, fetch tag property to find out the tapped cell index.
Your CollectionView will be contained within some kind of cell. Once you have found this cell, you can ask the table for the index. Navigate from your CollectionView up the view hierarchy to find the cell. For example:
CollectionView* collectionView = // your collectionView;
UITableViewCell* cell = (UITableViewCell*)[collectionView superview];
UITableView* table = (UITableView *)[cell superview];
NSIndexPath* pathOfTheCell = [table indexPathForCell:cell];
NSInteger rowOfTheCell = [pathOfTheCell row];
NSLog(#"rowofthecell %d", rowOfTheCell);
Related
I have 3 collection views on 1 view controller. I've tried a few of the other suggestions I've found on Stack but nothing seems to work.
All 3 Collection Views are in a separate cell of a HomeTableViewController. I tried to create the outlet connection to the HomeTableViewController but I get the error Outlets cannot be connected to repeating content.
I've read many people being able to hook up their multiple collectionViews so I am a bit confused as to where I'm going wrong...
The UICollectionView instances cannot be hooked up to IBOutlet properties in a separate UITableViewController.
As you describe, the UICollectionViews are actually each children of their own parent UITableViewCell, and as such are not direct descendants of the UITableViewController. This is because the cells will be added to the UITableView at run time.
If you are set on creating the outlets within the HomeTableViewController I would suggest creating them like so:
private weak var collectionViewA: UICollectionView?
and overriding cellForRow like so:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
// cast cell as collection view parent
collectionViewA = cell.collectionView
return cell
}
As has been mentioned, the better solution would be to manage the UICollectionView instances from within their own UITableViewCell parents. Example:
final class CollectionViewAParentTableViewCell: UITableViewCell {
#IBOutlet private weak var collectionView: UICollectionView!
}
extension CollectionViewAParentTableViewCell: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
…
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
…
}
}
You should create outlet in the UITableViewCell. Then you can provide tags to collectionViews in each cell in tableView:cellForRowAtIndexPath Method:
yourCell.collectionViewOutlet.tag = indexPath.row + 1000
you should replace 1000 with Any Constant Integer if the tags conflict with tags of other views.
Then use these tags to differentiate all collectionviews in collectionview:cellForItemForIndexpath method:
if(collectionView.tag == 1000){
//Code for collection view in first row of the table
}
else if(collectionView.tag == 1001){
//Code for collection view in second row of the table
}
else if(collectionView.tag == 1002){
//Code for collection view in third row of the table
}
You should also keep in mind to return number of items for each collection view just like above.
Tags make the life whole lot easier , don't they?
Happy Coding (Y)
You should create outlet in the UITableViewCell. Then you can provide tags to collectionViews in each cell in tableView:cellForRowAtIndexPath Method:
yourCell.collectionViewOutlet.tag = indexPath.row + 1000
you should replace 1000 with Any Constant Integer if the tags conflict with tags of other views.
Then use these tags to differentiate all collectionviews in collectionview:cellForItemForIndexpath method:
if(collectionView.tag == 1000){
//Code for collection view in first row of the table
}
else if(collectionView.tag == 1001){
//Code for collection view in second row of the table
}
else if(collectionView.tag == 1002){
//Code for collection view in third row of the table
}
You should also keep in mind to return number of items in collectionView:numberOfItemsInSection for each collection view just like above.
Tags make the life whole lot easier , don't they?
Happy Coding (Y)
Create three different collection view with different collection view cell, then after you just need to add in dataSource method like below:-
if collectionView == collectionViewA{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellA", for: indexPath) as! collectionCell
return cell
}else if collectionView == collectionViewB{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellB", for: indexPath) as! collectionCell
return cell
}else if collectionView == collectionViewC{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellC", for: indexPath) as! collectionCell
return cell
}else{
return UICOllectionViewCell()
}
also perform same for other dataSource method.
I have a table view with 3 types of custom cells.
Interface in storyboard.
3 different classes in the project for the cells.
I'm doing this currently and the cells IBOutlets are coming out to be nil.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = ListCell()
cell.configureCell(Data[indexPath.row])
return cell
}
class ListCell : UITableViewCell {
#IBOutlet weak var selectButton: UIButton!
func configureCell(data: ParamList) {
selectButton.setTitle("Select " + data.name, forState: .Normal)
}
#IBAction func selectButtonPressed(sender: AnyObject) {
print(selectButton.currentTitle)
}
}
I don't want to reuse any cells because every cell has different properties.
Some have textboxes other have different interactive matter which needs to be submitted in the end to a webservice.
I want to create new cells in memory for every cell.
Max cells will be around 15. So won't have much of a memory problems.
Any way I can Do that??
I did finally solve this.
Here's what I did for the same.
I am describing my process if anyone else wanna do the same.
Warning, only do this if absolutely necessary and you do not have that many cells that can affect the performance.
For making UITableView's cells to remain in memory even if scrolled outside the screen encapsulate your table view in a scroll view, and set your UITableView's scrollEnabled to false.
If using Storyboards set a height constraint to your TableView and make an IBOutlet to your code file.
Programatically add new cells with different identifiers.
"\(indexPath.section)\(indexPath.row)"
After getting your data, set your tableView's height to a multiplier of your data count.
i.e.
tableViewHeightConstraint.constant = heightOfOneCell * data.count
tableView.delegate = self
tableView.dataSource = self
This way you can scroll with the scrollView and all your cell will still be in the memory.
Set nil inside the method dequeueReusableCellWithIdentifier. So you can get what number of cells you want in memory.
I would like to confirm the approach I took to solve an issue with dequeuing custom cells in a UITableView as it scrolls such that the cells do not contain the old cell's data...
The app that contains a UITableView with custom UITableViewCells ("CustomCell"). Each CustomCell contains a UIStackView with one or more custom views via a nib ("CustomView"). I reuse the CustomCell as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierCustomCell, forIndexPath: indexPath) as! CustomCell
configureCell(cell, atIndexPath: indexPath)
return cell
}
The issue was that the cell would contain "old" data as the cell was being reused. To fix this, I override the prepareForReuse method in CustomCell as follows:
override func prepareForReuse() {
super.prepareForReuse()
for case let view as CustomView in stackView.subviews {
view.removeFromSuperview()
}
}
Then in layoutSubviews, I add the subviews back in:
override func layoutSubviews() {
super.layoutSubviews()
if stackView.subviews.isEmpty {
addCustomViewsToCell()
}
}
Performance seems fine thus far, but curious if this is a proper approach or if I will run into issues with scale in the future. I have not been able to find another workable approach thus far.
Thanks
Your code to reuse cells is correct. A common approach is to configure your cell's data within the cellForRowAtIndexPath function by setting a variable or calling a function on your custom cell:
let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierCustomCell, forIndexPath: indexPath) as! CustomCell
cell.data = myData[indexPath.row] // where myData is an array of type [Data]
return cell
Your cell would be in charge of its own layout to display the new data:
var data: Data {
didSet {
// configure and refresh your UI here
}
}
I suspect your issue has to do with your configureCell function. If you can, move this code into your cell's logic instead. This will be cleaner and easier to understand.
As far as performance, you might be fine now if your stack views don't have much content in them, but if they continue to grow in complexity you might see frame rate drops on older devices.
I have been looking around for solutions on how to do this but I am getting a bit confused.
I'd like to display tables of data in my app, not in the sense of Apple's TableView, more like tables you find in Excel or the in HTML.
Moreover in my app I will have multiple tables like this in different pages, that I wish to style the same way, not all with the same number of rows or columns. Those tables might be displayed on the screen alongside images or texts, probably never on their own. (so I need an element I can put in a UIViewController)
Basically a table like this (I put this off Google Images because my data is confidential)
I have the theory on how to do this but I'm not sure it's right or exactly how to do it.
My theory is I should create a subclass of UICollectionView, let's call it MyTablesView. MyTablesView would have a variable named data. My pages/VC that contain a table will have an instance of MyTablesView and feed it data in this variable. MyTablesView takes care of styling and displaying this data.
I'm fairly new to Swift and I don't know exactly so I'm not sure on how to tackle this. I am very confused on how to code a UICollectionView. I tried to read this tutorial: http://www.raywenderlich.com/78550/beginning-ios-collection-views-swift-part-1 but it's with a UICollectionViewController and not a UICollectionView so I'm not sure how to tweak it.
So for now this is all I have...
class MyTablesView: UICollectionView{
var data = [String]
}
(I put string for now because I'm not 100% sure of the data that would be in the tables, it will probably end up being doubles, but I'll also need something for headers...)
So I guess my question is:
How do I subclass a UICollectionView properly to display the data that will be given to it by a ViewController?
Let me know if anything is unclear.
Thanks in advance.
Well, turns out I didn't search thoroughly enough on SO, because here is the answer I had been looking for:
How to make a simple collection view with Swift
Since it's another Stack Overflow topic, should I copy the answer or trust Stack Overflow to keep the link alive?
The answer I linked to displays a grid of data, but I wanted a table, which means I had to tweak it a bit since I wanted rows so below is an overview of my code. Most of it is similar to the answer I linked to. The changes I made regards the data to display in a multidimensional array, which means I also had to add the numberOfSectionsInCollectionView method in my class and adapt the other methods accordingly.
I still need to do some styling to get it to look right.
In my storyboard I have a VC where I dragged a UICollectionView. In its first cell I dragged a UILabel.
The VC implements UICollectionViewDataSource and UICollectionViewDelegate and has a parameter named data containing the data to display.
From the storyboard I ctrl+dragged the UICollectionView and named it mytable.
class MyView: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
#IBOutlet weak var mytable: UICollectionView!
var data = [[String]]()
override func viewDidLoad() {
data = [["first", "1", "2"], ["second", "3", "4"]]
}
// --- UICollectionViewDataSource protocol ---
//returns number of sections = subarrays in data
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int{
return data.count
}
//returns number of items in a section= number of elements in a subarray in data
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data[section].count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! MyTableCells
// Use the outlet in our custom class to get a reference to the UILabel in the cell
cell.myLabel.text = data[indexPath.section][indexPath.item]
return cell
}
// --- UICollectionViewDelegate protocol ---
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// handle tap events
print("You selected cell #\(indexPath.item)!")
}
}
I also created a subclass of UICollectionViewCell.
From the storyboard I ctrl+dragged the UILabel from the first cell of the UICollectionView into my subclass and named it myLabel.
import Foundation
class MyTableCells: UICollectionViewCell {
#IBOutlet weak var myLabel: UILabel!
}
In the storyboard I hooked delegate and datasource to the VC (Select the UICollectionView, click on the circle and drag it to the VC)
I only did it for one of my VC for now, I might actually subclass UIViewController to be able to have multiple VC with this implementation I guess.
I've seen many tutorials online pertaining to embedding UICollectionViews inside dynamic table view cells and subclassing the collection view to set the delegate but I was wondering if the process is any different for static table view cells.
I tried following the example here but I couldn't quite follow it through because it seems overly complicated with little to no explanation. Would anyone mind outlining the basic steps I need to go through with in order to get my collection view to work?
Here's my code so far:
class FeaturedController: UITableViewController, UIScrollViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
popularCollectionView.dataSource = self
popularCollectionView.delegate = self
}
//MARK: Popular Events Collection View
#IBOutlet weak var popularCollectionView: UICollectionView!
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = popularCollectionView.dequeueReusableCellWithReuseIdentifier("popular", forIndexPath: indexPath) as! UICollectionViewCell
return cell
}
Many thanks!
If you know how to add a collection view to a dynamic table view cell, adding it to a static one would be far easier. You don't need to sub class anything at all (but doing so could be a nice idea). Under the hood, a static table view is nothing more than just a normal table view that having support from the hosting UITableViewController to set what you have layout in the Interface Builder automatically. So, here's how to do it:
Dragging a Collection View from the Object Library in the Interface Builder and place it in the cell you want.
Make sure the Table View is hosted by a Table View Controller.
Set constraints or layout the Collection View in the cell.
Set the Collection View's dataSource to the hosted Table View Controller.
Adding UICollectionViewDataSource conformance to the Table View Controller.
Implement UICollectionViewDataSource's methods, namely collectionView:numberOfItemsInSection: and collectionView:cellForItemAtIndexPath: in the UITableViewController.
If you know how to work with a UITableView or UICollectionView in general, this should not be hard to follow.
UPDATE
Your code looks like it should have worked.
So, you should check whether:
You've really set the class of the Table View Controller to your FeaturedController class.
You really have wired the Collection View in the Interface Builder to popularCollectionView.
You already have a prototype Collection View Cell with the identifier popular. Although, it should crash if you haven't done so.
In the IB, you have already set the Table View to be static.
I have a small example I did here
The orange view is the Collection View with the greenish view the prototype Collection View Cell with identifier myCell.
And then I set my view controller to the Collection View's data source. But you could set it in the code too like you did too.
Then I implement the data source methods below.
#interface ViewController () <UICollectionViewDataSource>
#end
#implementation ViewController
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"myCell" forIndexPath:indexPath];
return cell;
}
#end
And this is the result:
For TableView static cells:
In your view controller where you have connected collection view, add in view did load delegates and data source for the collection view, this will add the delegates to tableview cell, if you do with storyboard, you are connecting it wrong because delegates and datasource has to be inside cell initialization.
#IBOutlet weak var quotesCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
quotesCollectionView.delegate = self
quotesCollectionView.dataSource = self
}
For TableView dynamic cells:
class QuotesTableViewCell: UITableViewCell {
// MARK: Properties
#IBOutlet weak var quotesCollectionView: UICollectionView!
// MARK: Initialization
override func awakeFromNib() {
super.awakeFromNib()
quotesCollectionView.delegate = self
quotesCollectionView.dataSource = self
}
}