I have a UITableView where I allow the user to select cells and when they do I set the cell.accessoryType to .checkmark. My app is set up in a way that the user is supposed to go to the "main" ViewController, segue to the TableView, select a few cells, and then repeat this process a few times. My problem is that I don't want the tableView selections the user has already made to disappear. I have already tried self.clearsSelectionOnViewWillAppear = false and it does not work. So I made an array, that contains all of the indexPaths for the cells the user has selected, so when the user goes to the "main" ViewController and comes back I want my app to essentially reselect those cells.
Here is my code for this process...
for row in selectedRows {
print("Reselecting: \(row)")
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: row)
print(cell)
cell.accessoryType = .checkmark
}
When I print the cell it shows up in the log like this <UITableViewCell: 0x7fc2f90f3e00; frame = (0 0; 375 44); text = 'Title'; clipsToBounds = YES; layer = <CALayer: 0x600000aae260>>. One thing I noticed is that the text on my cell when it is properly displayed is not Title like the log suggests (Although it is the default text in my storyboard), could this have something to do with my problem? Why are the cells not check marked after running this code?
I think you should use tableView(_:cellForItemAt:) from UITableViewDataSource protocol.
func tableView(_ tableView: UITableView, cellForItemAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "<your reusable identifier", for: indexPath) as! YourCellSubclassIfAny
cell.accessoryType = isIndexPathSelected(indexPath) ? .checkmark : .none
// do whatever setup you need
return cell
}
Of course you need to set the dataSource of your tableView (if not using UITableViewController. Usually it is set to the view controller which contains the tableView. You can do it in storyboard or in code.
Related
I'm using this condition to create specific UICollectionViewCells.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if(indexPath.item % 3 == 0){//multiples of 3
let cell = myCollectionView.dequeueReusableCell(withReuseIdentifier: "Cell1id", for: indexPath) as! Cell1
cell.backgroundColor = .white
if(cell.wasCreated){
cell.cellLabel.text = "Cell(1) \(indexPath.item) was created before."
}
else{
cell.cellLabel.text = "Creating cell(1) \(indexPath.item) first time."
cell.wasCreated = true
}
return cell
}
else{
let cell = myCollectionView.dequeueReusableCell(withReuseIdentifier: "Cell2id", for: indexPath) as! Cell2
cell.backgroundColor = .gray
if(cell.wasCreated){
cell.cellLabel.text = "Cell(2) \(indexPath.item) was created before."
}
else{
cell.cellLabel.text = "Creating cell(2) \(indexPath.item) first time."
cell.wasCreated = true
}
return cell
}
}//end cellForItemAt
Here 'wasCreated' is a variable within the cell that I'm using to check if the cell is being created for the first time, if it is I set wasCreated = true and this should be done, for the first time, for every cell but it isn't.
The condition is: if the indexPath.item is a multiple of 3, deque cell 1 otherwise cell 2.
Now normally, when a cell is to be displayed for the first time the cell's init() method is called but in this case it's not called and the older cell is being dequed for some reason.
I have no idea why this is happening.
I have uploaded a sample project that reproduces the problem. Here's the link: https://github.com/AfnanAhmadiOSDev/IndexMultiplesTest.git
The behaviour you describe is expected. To reduce memory use, cells are reused as the collection view scrolls.
When you call dequeueReusableCell UIKit checks to see if there is a cell with the requested identifier that has moved offscreen and is therefore eligible for reuse. If there is, then this cell is returned. In this case init will not be called. If there is no candidate cell then a new cell instance is returned and init will be called.
When you run your code you will see that cells are being created at first, but after you scroll up and down sufficiently to build up a large enough cell reuse pool cells are re-used and no new cells are created.
Cell reuse is independent of the IndexPath for which the cell was previously used.
In the scenario mentioned in the question title, on changing the segment, ideally the UITableView should reload and hence the UITableViewCell should also reload. The issue is, all the content gets updated like label texts. But if I have expanded a subview of cell in one segment, it remains still expanded after segment is changed.
Segment index change function :
#IBAction func segmentOnChange(sender: UISegmentControl)
{
// Few lines
// self.tableMenu.reloadData()
}
Screenshot 1 :
Screenshot 2 :
So ideally, in screenshot 2, the cart view should have been collapsed.
Update :
Show/Hide view :
func showHideCartView(sender: UIButton)
{
let cell = self.tableMenu.cellForRow(at: IndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) as! RestaurantMenuItemCell
if sender.tag == 1
{
// Show cart view
cell.buttonArrow.tag = 2
cell.viewAddToCart.isHidden = false
cell.constraint_Height_viewAddToCart.constant = 50
cell.buttonArrow.setImage(UIImage(named: "arrowUp.png"), for: .normal)
}
else
{
// Show cart view
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
self.tableMenu.reloadData()
}
cellForRowAtIndexPath :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
cell.selectionStyle = .none
let menuItem = self.menuItems[indexPath.section]
cell.imageViewMenuItem.image = UIImage(named: "recommend0#2x.png")
cell.labelName.text = menuItem.name
cell.labelDescription.text = menuItem.description
cell.labelPrice.text = String(format: "$%i", menuItem.price!)
cell.buttonArrow.accessibilityHint = String(format: "%i", indexPath.section)
cell.buttonArrow.addTarget(self, action: #selector(self.showHideCartView(sender:)), for: .touchUpInside)
return cell
}
Since you rely on the sender button's tag to determine whether the cell should be shown in expanded or collapsed state, you need to make sure that when the segment changes, the tags for all the cells' buttonArrow also change to 1.
Unless that happens, the cells will be reused and since the buttonArrow's tag is set to 2, it will be shown as expanded.
You are reusing cells, see first line of your cellForRowAt implementation:
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
That means that a tableView does not create a new cell, nor it redraws it unless necessary for the given indexPath. However, in the code you expand a cell by setting some height constraints and isHidden flags on subviews of the cell. Now once you reload table at a new segment, the tableView will use the already rendered cells to make the reload as efficient as possible. However, that means, that although you will change the data in the cell, unless you explicitly collapse it, it will stay expanded (because previously you have set the constraints to expanded height).
You need to reset the expanded state when you change the segment. Now let's elaborate, because I believe your current solution has a hidden bug in it:
First of all, since as I said you are dequeueing the cells, in case there are a lot of items in the table and you are scrolling through them, there is a high chance that the expanded cell will get reused even somewhere later in the table view. So it will seem like there is some random item expanded too.
Therefore I suggest you to provide some model that would remember which cells are supposed to be expanded and then use the information in this model to update state of each cell before you return them in your cellForRowAt. The model can be for example a simple array of integer values that would represent indexes of cells that are expanded - so for example if there is an index 3 in the array, that would mean that cell at row 3 should be expanded. Then when you dequeue a cell in cellForRowAt for indexPath with row = 3 you should set constraints on that cell before returning it. This way you can remove the bug I mentioned. Moreover, then when you change segments, you can just remove all the indexes from the array to signify that no cell should be expanded anymore (do it before calling tableView.reloadData()).
What happened here, When you click any row then It will be expanded based on make true first condition in showHideCartView method and after that when you are changing segment index that keep height of previous row as it is because you are reusing cell So you must set normal hight of each row when you change segment index.
For do that take object of indexPath like
var selectedIndexPath: NSIndexPath?
Then set selectedIndexPath in showHideCartView method
func showHideCartView(sender: UIButton) {
selectedIndexPath = NSIndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) // you can change row and section as per your requirement.
//// Your other code
}
Then apply below logic whenever you are changing segment index.
#IBAction func segmentOnChange(sender: UISegmentControl)
{
if indexPath = selectedIndexPath { // check selectedIndexPath is not nil
let cell = self.tableMenu.cellForRow(at: indexPath) as! RestaurantMenuItemCell
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
selectedIndexPath = nil // assign nil to selectedIndexPath
// Few lines
self.tableMenu.reloadData() // Reload your tableview
}
I am trying to build an app with 2 cell on a tableview
the header that will have the Headlines and the second that will have the normal feeds.
My question is how to connect this two cells
The code for the raw feeds are working
Example:
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
// Configure the cell...
let post = self.posts[indexPath.row]
cell.title.text = post.title?.utf8Data?.attributedString?.string
if let imageUrl = post.imageUrl {
cell.imgView?.downloadImage(from: imageUrl)
}
let myFormatter = DateFormatter()
myFormatter.dateStyle = .none
myFormatter.timeStyle = .short
cell.pubDate.text = myFormatter.string(from: post.pubDate!) // What gives?
return cell
}
}
You can do this by filling a solid color, like white, for both the cells and set Table View separator to none.
tableView.separatorStyle = .none
And if you want to create a serious of such combined cells, then add a view to the bottom of the second cell, i.e. Feed Row, and fill that view with other color, like grey. This view will work as a separator for serious.
I suggest you let your tableview empty in storyboard. (delete all cells)
What I usually do, and what I feel is more flexible and cleaner in terms of code and files, is always use custom cells.
Create a subclass of UITableViewCell for each cell type you need (here, 2). One could be HeaderCell, and the other could be FeedCell.
If you use the "Create UITableViewCell class" I think you can find in Xcode (haven't used it in a long time), it should create the class for you, as well as a .xib file. If you don't have the corresponding .xib just create it and link it manually.
The .xib file is where you will put your labels, connect the outlets and maange everything you need. That's where you'll remake the cells you deleted ealier on. I think you can actually cut and paste in the xib and it might work.
Now you have a custom class, with its corresponding xib, which you can use as a cell.
Make sure your tableview is connected as delegate and datasource in storyboard (it should already be done in your setup).
In ViewDidLoad you will need to register all your cell classes to the tableview.
That is done using this method call on your tableview property (outlet)
I'm not sure how to write it in swift, but it's
MyTableView.Register(nib, key) in pseudo code.
Both the parameters come from your custom cell class (it's the .Nib and the class name, which you could hardcode). I usually do MyHeaderCell.Nib and MyHeaderCell.Key that are put in static, but that you can do however you want.
Now that your tableview is aware of the cells it will have to display, you just have to manage it in your cellForRow method.
Simply do it by index :
var cell;
if (indexPath.Row == 0)
{
cell = tableView.dequeueReusableCell(withIdentifier: "HeaderCell", for: indexPath) as! HeaderCell
}
else
{
cell = tableView.dequeueReusableCell(withIdentifier: "FeedCell", for: indexPath) as! FeedCell
}
let post = self.posts[indexPath.row]
cell.title.text = post.title
return cell
And there you go :)
I've never done swift so there are certainly syntax mistakes but the idea is how you should do it.
When I look to run this on my phone, no checkmark is assigned to the cell after I segue back and forth from my detailViewController. They currently have no checkmark assigned, so I am looking to assign a checkmark to the cell once it has been selected. PLease let me know if you know what is going on here! Thanks
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
if cell.accessoryType == UITableViewCellAccessoryType.None
{
cell.accessoryType = UITableViewCellAccessoryType.Checkmark
}
}
Currently you are dequeuing a new cell, not necessarily the one being selected. However,you should persist whether or not the cell is checked using a model object, not persist state within the view itself. Once the cell is dequeued/reused, it'll no longer be selected.
For example, you'd have an array of model objects:
class YourObject {
var isSelected: Bool = false
}
var modelObjects: [YourObject] = []
Inside each object there would be a property representing whether or not the object is selected, this would be set inside didSelectRowAtIndexPath.
var object = self.modelObjects[indexPath.row]
object.isSelected = true
tableView.reloadRowsAtIndexPaths([indexPath], rowAnimation: .Automatic)
Inside cellForRowAtIndexPath you can check if the property on the model object is selected, if so, set the cell's accessoryType to .Checkmark.
var object = self.modelObjects[indexPath.row]
if object.isSelected {
cell.accessoryType .Checkmark
}
The problem is that dequeueReusableCellWithIdentifier dequeues a cell that is not being currently displayed by the tableView, it does not return the cell that it's being displayed at that index path.
What you want to use is cellForRowAtIndexPath, which will give you the cell for that index path if it's being displayed by the tableView.
If you set the accessoryType to that cell, it will update the visible cell.
The way you are doing, you are dequeuing a new cell instead of updating the cell that is being selected. In order to achieve what you want you shoul replace this let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) with let cell = tableView.cellForRowAtIndexPath(indexPath).
We must handler the 'if' and 'else' both.
In 'cellForRowAtIndexPath':
private var selectedIndex: Int = -1
if selectedIndex == indexPath.row
{
cell.accessoryType = UITableViewCellAccessoryType.Checkmark
}
else
{
cell.accessoryType == UITableViewCellAccessoryType.None
}
I have setup a segue that will show a view controller with a small TableView. I want a different segue to show a bigger TableView but I want the bigger table to have the same exact info as the smaller table. Got the smaller tableView working perfect on its own, but once I give the bigger table a Data source, reset and try it out.....crashes.
// IndexPath or First Cell in TableView
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if self.TaskTableViews.hidden == false {
cell = tableView.dequeueReusableCellWithIdentifier( "FirstTask" , forIndexPath: indexPath) as UITableViewCell!
let list = frc.objectAtIndexPath(indexPath) as! List
cell.textLabel?.text = list.taskName
cell.textLabel?.textColor = UIColor.whiteColor()
TaskTableViews.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
TaskTableViews.layer.cornerRadius = 8
TaskTableViews.separatorColor?.colorWithAlphaComponent(2.0) }
if self.TaskTable2.hidden == false {
cell = tableView.dequeueReusableCellWithIdentifier( "Second Task" , forIndexPath: indexPath) as UITableViewCell!
let list = frc.objectAtIndexPath(indexPath) as! List
cell.textLabel?.text = list.taskName
cell.textLabel?.textColor = UIColor.whiteColor()
TaskTable2.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
TaskTable2.layer.cornerRadius = 8
TaskTable2.separatorColor?.colorWithAlphaComponent(2.0) }
return cell as UITableViewCell
}
The problem is that the code for your two tables is slamming into each other. To fix this, rejigger your logic. Do not make your logic depend on what is hidden. You can handle only one table at a time; just one table is calling here. That table comes in as the tableView parameter. Make your logic depend on that. Depending what table view that tableView parameter is, configure the cell and return it for that table view.
I think that you should consider changing your approach by just resizing the table view dynamically when you go from one scene to the other instead of having two table views if the information is exactly the same.
If you're still pushing for this approach then don't make the condition be that the table view is hidden and instead implement your own logic or boolean to determine this. But again, I'd rather resize a single table view as needed.
You have many problems in your code
1.
var cell = UITableViewCell()
What's the point of this line?
2.
cell = tableView.dequeueReusableCellWithIdentifier( "FirstTask" , forIndexPath: indexPath) as UITableViewCell!
What's the point of casting? This function returns UITableViewCell (not even optional)
3.
cell.textLabel?.text = list.taskName
Should not compile, cause UITableViewCell doesn't have textLabel
4.
TaskTableViews.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
What's the point of doing this every cell request? Move this to viewDidLoad or other appropriate place
Your if { } parts are identical except of reuse identifier
Use tableView argument when needed
Use if { } else
8.
return cell as UITableViewCell
Why cast? Just return
I'm sure I haven't found all of them))