Update array data used to create UITableView when IBAction is fired - ios

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

Related

Adding a cell to another TableView. (selecting an item from a tableview, showing it in another tableview)

I'm building an app which which has built in with 2 different tabs. First tab is is "Home" which basically has a tableview with cells that configured from an api.(The api gets me country names for now)
Those cells also have a "Star" button which prints the data of the specific cell for now.
Second tab is "Saved" tab(SavedViewController), where I want to show the "starred" countries, using a tableview.
You can see the image below in order to get an idea for the app.
App simulation Image
The star button has a function in my CountriesTableViewCell. I'm using a saveButtonDelegate in order to let the SavedViewController know about an item is going to be saved. The code in CountriesTableViewCell for star button is as below.
#objc func buttonTapped() {
//If Button is selected fill the image. Else unfill it.
if !isSaveButtonSelected {
saveButton.setImage(UIImage(systemName: "star.fill"), for: .normal)
isSaveButtonSelected = true
saveButtonDelegate?.saveButtonClicked(with: countryData) //Checking if save button is clicked
}
}
countryData is the data that I get from the api, and this is the data I want to pass to SavedViewController.
struct CountryData: Codable {
let name : String
}
So on the SavedViewController, I'm handling the data using the SaveButtonProtocol conformance as below:
extension SavedViewController: SaveButtonProtocol {
func saveButtonClicked(with data: CountryData) {
countryDataArray.append(data)
print("saveButtonClicked")
print("countryData in savevc is \(countryDataArray)")
DispatchQueue.main.async {
self.countriesTableView.reloadData()
}
}
}
Whenever I click the star button on the first tab, this function is getting called on SavedViewController. So whenever I click to button, those print statements above work fine.
The problem is, whenever the star button is clicked, it should append the data of the current clicked cell to countryDataArray in SavedViewController. But the array is not populating as it should.
Let's say I pressed the first cell's star button, my print("countryData in savevc is (countryDataArray)") statement prints : ["Vatican City"], then I press the second cell's star button it only prints ["Ethiopia"] while it should print ["Vatican City", "Ethiopia"]
Why this issue is happening? My best guess is I'm delegating SavedViewController from the cell class so it behaves differently for every other cell. If that is the problem what should I do to solve it?
Many Thanks.
You should store your data in a shared (static array) object so you only have one source and add the saved indicator in your country struct so you do not rely on what is displayed in one view controller.

Simultaneously change display parameters on all table view cells

I am trying to implement a table view design where a user can click a button outside of a table view cell and the display mode of all the buttons should change. However this is not the 'selected' mode for a given cell (that will be yet a third state that becomes accessible via switching to this second state). What's the proper way to accomplish this?
I am using dequeueReusableCellWith so I don't want to simply cycle through every cell because some that are out of sight probably shouldn't be modified. I simply want any cell that is visible, or becomes visible, while the table view cell is in this second display mode to follow a second design rather than the first design.
The second design, for now, is being modified via a method I added to a subclass of UITableViewCell like so:
- (void) p_refreshDisplay {
if (self.editing) {
self.buttonToClearWidth.constant = 20;
self.buttonToClearLeadingWidth.constant = 20;
} else {
self.buttonToClearWidth.constant = 0;
self.buttonToClearLeadingWidth.constant = 0;
}
}
However, I'm not sure how to trigger this p_refreshDisplay for every visible (and to become visible) cell. It seems unwise to call this many times and refresh the table view. What would be the proper way to accomplish what I want to do?
You do what should be done for any table view change:
Update your data model or some flag as needed.
Either call reloadData on the table view or call reloadRowsAtIndexPaths:withRowAnimation: passing in indexPathsForVisibleRows as the list of rows to reload.
Implement cellForRowAtIndexPath to provide appropriate cells for the given data/flags.
It sounds like you should have a custom cell class that has one or more properties that can be set on the cell in cellForRowAtIndexPath so the cell can render itself properly based on the specified state.
You can achieve this by doing three things:
Establish some property that indicates the "mode" of the table, either a boolean or perhaps an enum if there are more than three states
Ensure that cellForRowAtIndexPath configures the cell appropriately based on the value of this property. This will ensure that newly displayed cells are configured correctly.
When the "mode" changes you can use the tableview's visibleCells property to update any currently visible cells:
for cell in tableview.visibleCells {
if let myCell = cell as? MyCustomCellClass {
myCell.setButtonStyle()
}
}

Swift 2.0, UITableView: cellForRowAtIndexPath returning nil for non-visible cells

Please don't mark this as a duplicate question because I have found no suitable answer for my query.
I have a table view with cells that contain text fields. I have a button at the bottom of the screen. The number of rows is greater than the screen can display, so some cells are not in view. I want that at any point when the button is pressed, all textfields be read and the text input be processed. I am unable to do so because apparently because of the reusability of cells, cells out of view do not exist at all and cellForRowAtIndexPath for those indexPaths gives a runtime error. I have read answers that say that this simply can't be done. But I find it hard to believe that there is no workaround to this. Please help. Thanks in advance!
This definitely can't shouldn't be done (accessing cells that are off screen, or implementing workarounds to allow it), for reasons of (at least) performance and memory usage.
Having said that there is, as you put it, a workaround. What you will need to do it change how you are storing the values in those text fields. Rather than iterating through cells and retrieving the text directly from the text fields you should store the text for each field in an collection.
Define a dictionary property on your table view controller / data source to hold the text for each row.
Act as the delegate of UITextField and assign as such in tableView:cellForRowAtIndexPath:
Implement textField:didEndEditing: (or whatever is appropriate for your use case) and store the text in the dictionary keyed against the index path relating to the cell that contains that text field.
Update the button action method to use this dictionary instead of iterating through cells.
Create a UITableViewCell subclass, add your tableViewCells a index property and introduce a delegate like:
protocol CustomCellDelegate {
func didEditTextField(test : String, atIndex : Int)
}
Create a delegate variable on your UITableViewCell subclass.
var delegate : CustomCellDelegate?
Implement the UITextViewDelegate in your tableViewCell and set the cell to be the delegate of your textfield.
Implement these two methods:
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField : UITextField) {
// call back with the delegate here
// delegate?.didEditTextField(textfield.text, atIndex: self.index)
}
So when the user ends editing the textField, your cell will call out with a delegate, sending the text and the index.
On your viewController, when you init your tableViewCell, set the delegate of the cell to the viewController, and set the index to indexPath.row .
Set up on your viewController a String array with as many items as many tableViewCells you got. Init the array with empty strings.
So you got the delegate on your viewController, and whenever it is called, you just insert the returned text to right index in the string array on your viewcontroller.
What do think, will this work?
If we can assume that cells that have NEVER been created has no text inputs, then create an Array or Set ourselves and clone the content/input texts there whenever user inputs to a cell's textfield.
Whenever, that button is clicked, you can iterate the array instead of cells to get all the inputs.
This is a bit hacky though..

TableViewCell button click firing in multiple sections of TableViewControl

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.

Call UITableViewCell Method from Method in main VC?

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.

Resources