Bit of a weird one i can't seem to understand so i have a tableview control in a viewcontroller and within the tableviewcell i have a button which is using a delegate.
So when the button is clicked it will change the tag number of the cell and also change the image of the button based on the tag of the cell in the tableview.
But the problem i'm having is that when you select a button in a row i.e. the first one it seems to be running the function to change the image for each item in a section almost. I.e choosing the 5th item will update every 5th button in a cell to the selected image which is weird. Since it's almost like the tableview is being split up to sections of 5.
Can anyone help prevent this behaviour so it only changes the icon for the button in the cell which has been pressed.
Below are the functions that i've used and a link to a video on dropbox showing the behaviour https://www.dropbox.com/s/a4aasti78872w6j/Help.mov?dl=0
Delegate
protocol StoryTableViewCellDelegate: class {
func didBookmarkItem(cell: StoryTableViewCell, sender: AnyObject)
}
Function for Delegate
#IBAction func bookmarkButtonDidTouch(sender: AnyObject) {
bookmarkButton.animation = "pop"
bookmarkButton.curve = "easeOut"
bookmarkButton.duration = 0.5
bookmarkButton.damping = 0.4
bookmarkButton.velocity = 0.4
bookmarkButton.animate()
// The delegate which will handle bookmarking items
delegate?.didBookmarkItem(self, sender: sender)
}
Function being used in tableview class
// MARK: StoryTableViewDelegate
func didBookmarkItem(cell: StoryTableViewCell, sender: AnyObject) {
// TODO: Implement bookmark functionality
switch cell.bookmarkButton.tag {
case 0:
cell.bookmarkButton.setImage(UIImage(named: "Bookmark-Selected"), forState: UIControlState.Normal)
cell.bookmarkButton.tag = 1
case 1:
cell.bookmarkButton.setImage(UIImage(named: "Bookmark-Not-Selected"), forState: UIControlState.Normal)
cell.bookmarkButton.tag = 0
default:
break
}
}
The reason is, you are probable creating a cell using :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellID")
...
like you should do. However dequeueReusableCellWithIdentifier re-uses a cell, so if you have changed anything in a cell (like adding buttons), those buttons will still be in that recycled cell.
So you must set the book mark status in your cellForRowAtIndexPath for each cell.
I once had a similar problem when adding buttons to a cell, and when scrolling the cells automatically had extra buttons. So the first thing I did when re-using a cell was remove all buttons.
Edit sample code
in your
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
you have to add code for setting the button to the desired state, it will be something similar to the code you're using in didBookMarkItem :
switch myProperty {
case 0:
cell.bookmarkButton.setImage(UIImage(named: "Bookmark-Selected"), forState: UIControlState.Normal)
cell.bookmarkButton.tag = 1
case 1:
cell.bookmarkButton.setImage(UIImage(named: "Bookmark-Not-Selected"), forState: UIControlState.Normal)
cell.bookmarkButton.tag = 0
default:
break
}
You'll need a myProperty variable of course, which you should change in your didBookMarkItem as well
As others have said, it's due to table cell reuse. You want your tableView(_:cellForRowAtIndexPath:) method to always update the cell state from your model. Your bookmark state should be a property of your model. When the bookmark button state for a cell is changed by the user, the new state should be reflected in your model. If you do these things, your table cell state will always be correct. If you don't already, it's nice to always define a custom UITableViewCell class that contains outlets for all of the views and controls within your table view cell (your bookmark button would have an outlet.) Also, within your custom cell class, when you capture the button selection, you can call back to the controller either via a delegate (as you are doing), or a "bookmarkChanged" handler (closure) defined on the cell. I prefer the closure method. If you would like to see an working example of this, let me know.
Also, nix the tag. You don't need the tag to keep track of your bookmark button selection state, unless there's more to it than you have shown/described. If you follow the approach I described, it's taken care of.
Table view cells are reused. Once you select a cell and change its tag number and scroll the selected cell out of the table view, it will be used again to show a row at the index path of the new row that appears. And since its tag represents that of a selected cell it is displayed as highlighted.
Try to look up about cell reuse in table views.
You need to change your approach about determining which cell is to be displayed as selected. Add it as a property in your data source and based on that set the bookmark as selected or not selected.
Related
I have created a UITableView that includes a UIImage, and two UILabels. This data is passed to a details view controller when the user selects the row. I have also successfully created a UIButton in the prototype cell that updates the text of one of the UILabels. I am unsuccessfully attempting to update this label within the array so this change is reflected on the details view controller. This is what I tried with no success:
#IBAction func updateTableData(_ sender: Any) {
grinds.removeAll()
grinds = createArray()
}
Here is a link to the repo: https://github.com/andrewTuzson/tableViewPractice
In the screenshot below, the left image displays the tableview after the first cell has been toggled to "Needs Work". The right image displays the details screen that is loaded after selecting the row.
How should I approach solving this?
If you want to update a tableViewCell upon the button click then, you should make the model array global to access it in click method and update what you want then reload the tableView either with delegate or NSNotificationCenter
--
in the custom class of the cell declare an integer property and in cellForRow set it like this
cell.index = indexpath.row
I need an event, that changes a variable based on which TableViewCell I click. But unlike an action connected to a button, there is no action indicator for table view cells at all. So my question is:
I want to make a TableView that contains items of an array. Based on which item I click, I want to change my variable so that the result on the next ViewController depends on which button you click.
So to make things easier, here is an example what I want the app to look like:
On the first TableViewController I have a list based on an array and on the second ViewController I have a label that shows text based on the variable.
I have a nameArray = ["Mum", "Brother", "Me"] and a weightArray = [140, 160, 120] and a variable weight = 0. The label on the second ViewController tells the var weight. So when you click on "Mum" in the TableView I want the next ViewController to say 140, when I click on "Brother" then 160 and so on...
Until here everything works just fine and I have no problems with anything but changing the var based on what I click.
Long story, short sense:
I want an Action for the TableViewCell that changes the var like in an Action connected to a Button, but there is no Action outlet for Cells at all.
Use this method. Use indexPath.row to find what row number you selected
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
switch cell.labelSubView.text as! String {
case "Mum":
self.weight = weightArray[0]
case "Brother"
self.weight = weightArray[1]
and so on..
..
default:
statements
}
}
Note A better alternative
I also considered a case where you have too many entries in nameArray and switch statement might not be good. In that case you can get the text inside the selected row by cell.labelSubView.text as! String
next you can check if the nameArray contains the cell text and get the index of the name that matches the cell text. Next you can get the required weight at the same index in weightArray. And then do self.weight = weightArray[requiredIndex]
Hope this helps.
Update : My experienced friend #Duncan mentioned down below that switch statement in this case is a bad coding practice . I am not going to delete it because it is a lesson for me and also my fellow programmers who are relatively new to programming. So i have put it in a yellow box, stating that it is not a good code
A better option for this would be :
As Duncan mentions, creating an array of dictionary is a good option
Second option is the option in my answer after my Note
You need to maintain array of dictionaries , those dictionaries have keys like "person", and "weight", then you can easily get weight value after selecting the cell by using table view delegate method UITableViewDelegate's tableView:didSelectRowAtIndexPath:
Create an instance variable in your view controller (a var at the top level after the class definition) for the selected cell.
class MyTableViewController: UIViewController
var selectedRow: Int
override func tableView(tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath)
{
selectedRow = indexPath.row
//invoke a segue if desired
performSegueWithIdentifier("someSegue");
}
override func prepareForSegue(segue: UIStoryboardSegue,
sender: AnyObject?)
{
if segue.identifier == "someSegue"
{
//Cast the destination view controller to the appropriate class
let destVC = DestVCClass(segue.destinationViewController)
destVC.selectedRow = selectedRow
}
}
As Andey says in his answer, it's probably better to create a single array of data objects (dictionaries, structs, or custom data objects). Then when the user taps a cell, instead of passing the index of the selected row to the next view controller, you could pass the whole data object to the destination view controller. Then the destination view controller could extract whatever data it needed. (Weight, in your example.)
I am trying to have two buttons as the first row and the rest of the rows as it is shown in the following picture (I apologize for the flipped picture):
I know how to put two buttons in a cell, like so:
And use the following code to add the first cell to my tableview like so:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
//Configure the first cell with the two buttons
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = cathyList[indexPath.row]
}
return cell
}
My logic allows me to have two buttons as the first row cell but I don't know how to make the buttons big and square like the ones that are shown here. Any suggestions?
You format your button as what you want on Storyboard. I suggest 2 options you can get this:
You can add image and title on button and use Edge property to achieve this. Look at this:
This way is hard to achieve what you need, you have to make sure your icon has the right size, and it's not dynamic.
The second way is easier and more dynamic. But maybe not the best solution. I usually use this way.
You add an image (show your icon), a label (your title) and embed them in a view. Then add a button stretch all the view. Connect this button to action and use it as usual.
Hope this help.
Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.
I'm trying to do the opposite of what most people on this topic are asking to do. Most people want a button within a table view cell to call a method in their VC / VC table. I already have a protocol doing that.
Problem / Question
What I am trying to add now is the opposite: I need a button press on my main ViewController (which houses my table) to call a method within my CusomTableViewCell class (note: the button pressed on the main VC is not in the table). I have the protocol class created and the function written, but I don't know how to set the CustomCellViewClass as the delegate. When I did the opposite, I inserted "cell.delegate = self" into the cellForRowAtIndexPath method. I've also used prepareForSegue to assign a delegate. But with no segue and now cell-creation-method, I'm lost!
Example of Desired Function
My end goal is that pressing a button that is in the main VC will change the title of a button within the cells. A simple example would be that I have one view with a single table, on button press the table contents switch between two arrays, cars and motorcycles. When the table is showing cars, the cell button titles should all read "Look inside" but when showing the motorcycle button it should read "Look closer".
Code
I've already written the function that I want the cell to execute:
func cellButton_Title_Switch (currentList: String) {
if vcState == "cars" {
cellButton.setTitle("Look inside", forState: UIControlState.Normal)
}
else {
cellButton.setTitle("Look closer", forState: UIControlState.Normal)
}
}
I created the protocol:
protocol delegateToChangeCellBut {
func cellButton_Title_Switch (currentList: String)
}
I have the self.delegate.cellButton_Title_Switch(currentList) within my VC button and the protocol added to my custom cell class declaration. But how do I do that last missing piece in the custom cell class, where I assign the class to the delegate?
My original problem was that my UITableView's cell has buttons and labels, some of which change to match the state of things outside the table, things handled by the mainViewController.
The custom cell is defined by a customCellviewController. All the custom cell buttons and labels have their IBOutlets connected to the customCellviewController. I couldn't figure out how to make an action/change outside the table (in the mainViewController) immediately cause the cell labels and buttons to change.
Note: Protocols tend to work they other way around (a cell action triggers a function in the mainVC). I couldn't figure out how to use a protocol to solve this. Luckily, the solution was much simpler than a protocol.
The Solution!
I wrote the "updateCell" method that would change the labels and buttons and that code now sits in the customCellviewController. Then I called/triggered the "updateCell" function from the mainViewController simply by adding the call into my cellForRowAtIndexPath function. So it looks something like this:
var stateOfPage = "Green"
//Creates the individual cells. If the above function returns 3, this runs 3 times
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Setup variables
let cellIdentifier = "BasicCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! customCell
cell.updateCell(stateOfPage)
return cell
}
Now the above code/method runs when the table gets built. So to update the cells, have some button tap or other action reload the table data:
tableView.reloadData()
Good luck.