I have a view controller containing a table view. In the table view are cells that have a button to delete the data the cell contains. When I press the delete button the cell information is removed from my database but the table view doesn't update until i segue back and forth.
I'm unsure how to access the TableView and force it to re-draw itself from a method inside my table cell.
class EditTaskTableViewCell: UITableViewCell {
//......//
#IBAction func deleteButtonPressed(_ sender: Any) {
let userDefaults = Foundation.UserDefaults.standard
if let userTasks = userDefaults.array(forKey: "userTasks"){
let newArray = removeTask(item: taskNameLabel.text!, from: userTasks as! [String])
userDefaults.set(newArray, forKey: "userTasks")
userDefaults.synchronize()
}
//update table view
}
}
class EditTaskTable_VC: UIViewController, UITableViewDelegate, UITableViewDataSource {
//......//
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Bundle
.main
.loadNibNamed("EditTaskTableViewCell", owner: self, options: nil)?
.first as! EditTaskTableViewCell
cell.taskName.text = cellData[indexPath.row].title
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellData.count
}
Try to use delegates.
The example is in Objective C but the general idea is there.
sending reload data from custom tableview cell?
swift delegate example
https://gist.github.com/austinzheng/db58036c4eb825e63e88
Related
I need the tableView to perform reloadData() after a row has been added via a textView. My tableViews all use the reusable code which works fine.
Below is my Reusable TableViewCode.
class ReusableSubtitleTable: NSObject, UITableViewDataSource, UITableViewDelegate{
let cell = "cell"
var dataArray = [String]()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) {
print("DataArray count from table view = \(dataArray.count)")
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let selfSizingCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SelfSizingCell
let num = indexPath.row
selfSizingCell.titleText.text = (stepText[num])
selfSizingCell.subtitleText.text = dataArray[num]
return selfSizingCell
}
}
The function below uses the reusable code to display the table which works fine.
class DetailViewController: UIViewController {
let step = 13
var tableView: UITableView!
let dataSource = ReusableSubtitleTable()
var selectedEntry: JournalEntry!
var dataModel = [String]()
var didSave = false
var coreDataManager = CoreDataManager()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = dataSource
tableView.dataSource = dataSource
dataSource.dataArray = dataModel
#IBAction func unwindToDetail( _ sender: UIStoryboardSegue) {
dataModel[10] = step11
didSave = coreDataManager.updateEntry(step11: step11, selectedEntry: selectedEntry)
}
}
The problem come in when a user wants to add to the last row. The user taps a button and is taken to the next controller which is a TextView. When user finishes their entry they tap the 'Save' button which returned to the DetailViewController via an unwind. The selectedEntry is saved and the dataModel updated. Now the table view needs to reload to display this added text.
I've tried adding tableView.ReloadData() after didSave. I've tried a Dispatch and tried saving the data before returning from the textView via the unwind but that doesn't work either.
I tried adding the below function to ReusableTableView and called it after the coredata update - there are no errors but it does not reload the table.
func doReload(){
tableView.reloadData()
}
I have verified that the data is saved and it does displays if I return to the summary controller and then go forward the DetailViewController.
Any help is appreciated.
Placing the UITableView reloadData() within either viewWillAppear or viewDidAppear should resolve this issue.
For example:
func viewDidAppear(_ animated: bool) {
super.viewDidAppear(animated)
tableView.reloadData()
}
This is because the view hierarchy isn't yet regarded as "visible" during the segue unwind and why you see it work by going back to reloading the view controller. The reloadData() function, for efficiency, only redisplays visible cells and at the time of the unwind the cells aren't "visible".
Apple Documentation - UITableView.reloadData():
For efficiency, the table view redisplays only those rows that are
visible.
Hello the image above is the UI of my todo list app, now I just want to show the detail of item (First Item, second Item etc) when I click the detail button in the tableviewcell. So in order to get the property of the item, I need to know the indexPath of the row that I just clicked on the detail button.
I have tried some properties of the tableview like didSelectRowAt, or indexPathForSelectedRow, but both not work. For didSelectRowAt user need to click on the row first then click the detail button, and that's not what I want, and the indexPathForSelectedRow is not working for me.
A common, generalized solution for this type of problem is to connect the #IBAction of the button to a handler in the cell (not in the view controller), and then use a delegate-protocol pattern so the cell can tell the table when the button was tapped. The key is that when the cell does this, it will supply a reference to itself, which the view controller can then use to determine the appropriate indexPath (and thus the row).
For example:
Give your UITableViewCell subclass a protocol:
protocol CustomCellDelegate: class {
func cell(_ cell: CustomCell, didTap button: UIButton)
}
Hook up the #IBAction to the cell (not the view controller) and have that call the delegate method:
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBOutlet weak var customLabel: UILabel!
func configure(text: String, delegate: CustomCellDelegate) {
customLabel.text = text
self.delegate = delegate
}
#IBAction func didTapButton(_ button: UIButton) {
delegate?.cell(self, didTap: button)
}
}
Obviously, when the cell is created, call the configure method, passing, amongst other things, a reference to itself as the delegate:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, delegate: self)
return cell
}
}
Finally, have the delegate method call indexPath(for:) to determine the index path for the cell in question:
extension ViewController: CustomCellDelegate {
func cell(_ cell: CustomCell, didTap button: UIButton) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath.row` here
}
}
The other approach is to use closures, but again using the same general pattern of hooking the button #IBAction to the cell, but have it call a closure instead of the delegate method:
Define custom cell with closure that will be called when the button is tapped:
class CustomCell: UITableViewCell {
typealias ButtonHandler = (CustomCell) -> Void
var buttonHandler: ButtonHandler?
#IBOutlet weak var customLabel: UILabel!
func configure(text: String, buttonHandler: #escaping ButtonHandler) {
customLabel.text = text
self.buttonHandler = buttonHandler
}
#IBAction func didTapButton(_ button: UIButton) {
buttonHandler?(self)
}
}
When the table view data source creates the cell, supply a handler closure:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, buttonHandler: { [weak self] cell in // the `[weak self]` is only needed if this closure references `self` somewhere
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath` here
})
return cell
}
}
I personally prefer the delegate-protocol pattern, as it tends to scale more nicely, but both approaches work.
Note, in both examples, I studiously avoided saving the indexPath in the cell, itself (or worse, “tag” values). By doing this, it protects you from getting misaligned if rows are later inserted and deleted from the table.
By the way, I used fairly generic method/closure names. In a real app, you might give them more meaningful names, e.g., didTapInfoButton, didTapSaveButton, etc.) that clarifies the functional intent.
Implement the delegate method tableView(_:accessoryButtonTappedForRowWith:)
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
However if you want to navigate to a different controller connect a segue to the accessory view button
If the button is a custom button see my answer in Issue Detecting Button cellForRowAt
I am creating a UITableView that enables the user to add a variable amount of data. Table looks like this initially:
When the user clicks on the "+" button, i would like to add a new cell with a UITextField for entering data. This new cell is a Custom UITableViewCell called "RecordValueCell". Here's what is looks like:
//Custom UITableViewCell
class RecordValueCell : UITableViewCell {
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var deleteButton: UIButton!
var onButtonTapped : ((_ sender : UIButton)->Void)?
#IBAction func deleteButtonTouched(_ sender: Any) {
guard let senderButton = sender as? UIButton else {
return
}
onButtonTapped?(senderButton)
}
}
However when i try to add another cell, using the tableView.dequeueReusableCell(withIdentifier: ) function, it seems to return the same cell. And here is what my UI looks like:
Empty space at the top of the section where my new cell should be. Here is the code to add the cell:
func addNewValueCell() {
guard let reusableValueCell = self.tableView.dequeueReusableCell(withIdentifier: "valueCell") as? RecordValueCell else {
fatalError("failed to get reusable cell valueCell")
}
var cell = Cell() //some custom cell Object
//add the gray horizontal line you see in the pictures
reusableValueCell.textField.addBorder(toSide: .Bottom, withColor: UIColor.gray.cgColor, andThickness: 0.5)
reusableValueCell.onButtonTapped = { (sender) in
self.removeValue(sender: sender)
}
cell.cell = reusableValueCell
self.sections[self.sections.count - 1].cells.insert(cell, at: 0)
//When i put a break point at this spot, i find that reusableValueCell is the same object as the cell that is already being used.
tableView.reloadData()
reusableValueCell.prepareForReuse()
}
When i debug it, i find that dequeueReusableCell(withIdentifier: ) returns the exact same RecordValueCell multiple times.
Here is my cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = self.sections[indexPath.section].cells[indexPath.row].cell else {
fatalError("error getting cell")
}
return cell
}
numberOfRowsInSection
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].cells.count
}
First of all, you will need to set the View Controller Class that this table is contained in as the table's UITableViewDataSource
tableView.dataSource = self // view controller that contains the tableView
Create an array of strings as member of your View Controller class which contains the data for each cell:
var strings = [String]()
Then you will need to implement the following method for the UITableViewDataSource protocol:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return strings.count
}
You should also be dequeueing the cells in your cellForRowAt method like so:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: yourIdentifier) as! YourCellClass
cell.textLabel = strings[indexPath.row]
return cell
}
Then whenever the user enters into the textField, their input will be appended to this array:
let input = textField.text
strings.append(input)
tableView.reloadData()
Once the data is reloaded, the cell will be added to the table automatically since the number of rows are defined by the String array's length and the label is set in the cellForRowAt method.
This feature is very easy to implement if you will do in a good way.
First, you have to create two TableCell. First to give the option to add a record with plus button and second for entering a value with textfield. Now always return first cell (AddRecordTableCell) in the last row in tableView, and return the number of rows according to entered values like
return totalValues.count + 1
I have viewcontroller and it has split by two views. one is view which cotrolled by tableviewcontroller which is childcontroller of viewcontroller, and one is statusview which is show data directly when tableviewcontrollersdidselectrowatindexpathis called. but when i cant reload my data in statusview when i called didselectrowatindexpath. i can relaod my data in tableview but status view does not reflect data.
the darkgray area is statusview and middle view is tableview.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.reloadData()
}
by this code i can reload my data in tableview . but when i use setneedsDisplay or setneedslayout of parentController(viewcontroller) it crash. how can i solve this problem?
In this case you will need one of these two options:
Create a protocol that your main UIViewController will implement, so that UITableViewController can pass data via the delegate to its parent view controller
Post a UINotification in the UITableViewController and receive this notification in your status bar view and display the data.
Lets explore both options:
Define a protocol, in this example I am only sending a String of the cell that was tapped on:
#objc protocol YourDataProtocol {
func didSelectCell(withString string: String)
}
Next add a delegate property to your UITableViewController
class YourTableViewController: UIViewController {
weak var delegate: YourDataProtocol?
...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
//call the delegate method with your text - in this case just text from textLabel
if let text = cell?.textLabel?.text {
delegate?.didSelectCell(withString: text)
}
}
}
Make your UIViewContoller be the delegate of the UITableViewController subclass:
class YourViewController: UIViewController, YourDataProtocol {
...
let yourTableVC = YourTableViewController(...
yourTableVC.delegate = self
func didSelectCell(withString string: String) {
statusBar.text = string//update the status bar
}
}
Second option is with using NotificationCenter
In your UITableViewController you post a notification
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
if let text = cell?.textLabel?.text {
let notificatioName = Notification.Name("DataFromTableViewCell")
NotificationCenter.default.post(name: notificatioName, object: nil, userInfo: ["YourData": text])
}
}
In status bar you start listening to this notification
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveData(_:)), name: notificatioName, object: nil)
#objc func didReceiveData(_ notification: Notification) {
if let userData = notification.userInfo, let stringFromCell = userData["YourData"] {
print(stringFromCell)
}
}
I've already looked at the post UITableView.reloadData() is not working. I'm not sure that it applies to my situation, but let me know if I'm wrong.
My app has a tableView. From the main viewController I am opening another viewController, creating a new object, and then passing that object back to the original viewController, where it is added to an array called timers. All of that is working fine. However, when I call tableView.reloadData() in didUnwindFromNewTimerVC() to display the updated contents of the timers array, nothing happens.
NOTE: I have verified that the timers array is updated with the new object. Its count increments, and I can access its members. Everything else in didUnwindFromNewTimerVC() executes normally. The tableView just isn't updating to reflect it.
Here is my code:
import UIKit
class TimerListScreen: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tabelView: UITableView!
var timers = [Timer]()
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tabelView.delegate = self
tabelView.dataSource = self
let tempTimer = Timer(timerLabel: "temp timer")
timers.append(tempTimer)
}
#IBAction func didUnwindFromNewTimerVC(_sender:UIStoryboardSegue){
guard let newTimerVC = _sender.source as? newTimerVC else{return}
newTimerVC.timer.setTimerLabel(timerLabel: newTimerVC.timerLabel.text!)
timers.append(newTimerVC.timer)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tabelView.dequeueReusableCell(withIdentifier: "TimerCell", for: indexPath) as? TimerCell{
let timer = timers[indexPath.row]
cell.updateUI(Timer: timer)
return cell
}else{
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timers.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 78
}
}
Thank you
Please note the spelling. There are two table view instances: the outlet tabelView and a (pointless) instance tableView.
Reload the data of the outlet
tabelView.reloadData()
and delete the declaration line of the second instance let tableView ....
However I'd recommend to rename the outlet to correctly spelled tableView (you might need to reconnect the outlet in Interface Builder).
And force unwrap the cell
let cell = tabelView.dequeueReusableCell(withIdentifier: "TimerCell", for: indexPath) as! TimerCell
and remove the if - else part. The code must not crash if everything is hooked up correctly in IB.