I have a table view with many table view cells. When the user clicks on a cell, I want to update the cell's label text.
Here is my table view controller class:
class MyTableViewController: UITableViewController {
var data = [Data]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
// Here I fetch and populate the data list
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "MyTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? MyTableViewCell else {
fatalError("The dequeued cell is not an instance of MyTableViewCell.")
}
let cellData = data[indexPath.row]
cell.initialize(data: cellData)
return cell
}
}
And here is my table view cell class:
class MyTableViewCell: UITableViewCell {
var data: Data?
#IBOutlet weak var nameLabel: UILabel!
func initialize(data: Data) {
self.data = data
if let cellName = data.name {
nameLabel.text = cellName
}
}
}
How can I change the text of the nameLabel above (to "Clicked") when the user clicks on the table view cell?
There're no doubt different ways you could handle this, but here is what I would suggest:
Implement the UITableViewDelegate methods tableView(_:didSelectRowAt:) and tableView(_:didDeselectRowAt:). Add a selected bool to the data model for the cells in your table view, and update the state of that bool as the cells are selected/deselected.
Then modify your cellForRow(at:) method so it uses the selected flag to decide what to show in your label.
Finally, have your tableView(_:didSelectRowAt:) and tableView(_:didDeselectRowAt:) methods tell the table view to reload the newly selected/deselected cell.
Related
I've created a tableView with prototype cells. Inside each of these prototype cells is another tableView with different prototype cells. I've linked this all together fine, but I'm having trouble modifying the innermost prototype cells. Here is why.
Here is the relevant code:
class ViewController: UIViewController, AVAudioRecorderDelegate, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "outerCell") as! outerCell
//would obviously make some modification to cell here, like cell.title = "test" or something
let cell2 = cell.commentTableView.dequeueReusableCell(withIdentifier: "innerCell") as! innerCell
cell2.commentText.text = "sus"
//NEED TO DIFFERENTIATE HERE ON HOW TO KNOW WHICH CELL TO RETURN
//e.g. NEED TO RETURN either cell1 or cell2, depending on the tableView
}
My code for outerCell looks like this:
import UIKit
class outerCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var commentTableView: UITableView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
commentTableView.delegate = self
commentTableView.dataSource = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "innerCell", for: indexPath) as! commentCell
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
}
See, the main problem is, both these table views work fine and all, but, in the first chunk of code, if I just do something like,
if tableView == self.tableView{
return cell }
else ...
this won't work, as tableView always seems to be self.tableView.
How can I modify my code so that I can actually impact the text displayed in the inner cell, and the outer cell, in the same block of code?
Also, please note, I know that, based on the example given here, there is no need for these nested cells. I've just simplified the code here to focus on what's important - my actual code has a lot of stuff happening in both the inner and outer cell.
Thank you, any help would be appreciated.
you need to first create two different cell classes.
In outer class :
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SearchPreferredJobTableViewCell
cell.responseCreateBookingObj = { [unowned self] (returnObject) in
DispatchQueue.main.async {
tableView.beginUpdates()
}
// do your logic
DispatchQueue.main.async {
cell.contentView.layoutIfNeeded()
tableView.endUpdates()
} }
return cell
}
// other cell class
Declare variable
var responseCreateBookingObj : APIServiceSuccessCallback?
// send callback from you want to send
guard let callBack = self.responseCreateBookingObj else{
return
}
callBack(true as AnyObject)
// also do in when user scroll it'll manage
tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath){
DispatchQueue.main.async {
tableView.beginUpdates()
}
// do your logic
DispatchQueue.main.async {
cell.contentView.layoutIfNeeded()
tableView.endUpdates()
}
}
I have table view in which cell consist of multiple sections, each section have different number of rows and every row have a text field. When I wrote in it and scroll down and up the data lost or reshuffled. So I am trying to save textfield data into the 2 dimensional array but I can’t solve this problem.
Here is code in Custom cell:
class CustomCell: UITableViewCell {
#IBOutlet weak var userName: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
View controller code:
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.text = "Header"
return label
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 7
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableview.dequeueReusableCell(withIdentifier: CustomCell.identifier, for: indexPath) as! CustomCell
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80.0
}
}
Your cells cannot be expected to maintain data (the contents of your UITextField) as they get removed from memory once they are outside the visible bounds of your table view.
You have to look into the UITableViewDataSource protocol and store the contents of your cell’s UITextFields in a class which will remain in memory for the duration of your table view’s view controller.
Typically, people use the view controller to be the Data Source as you have done.
Steps are as follows:
In your view controller's initialization, create and prepare a data structure (array / dictionary keyed on IndexPaths) that will contain the contents of the text you need to store
When dequeuing a cell (in your cellForRowAt function), configure the cell with the necessary string from your data structure, if content exists for that particular indexPath.
When the text is changed by the user in the cell, notify your data source of new contents for the cell's index path
Example:
Define the following protocol:
protocol DataSourceUpdateDelegate: class {
func didUpdateDataIn(_ sender: UITableViewCell, with text: String?)
}
Ensure your UITableViewCell declares a delegate variable and uses it:
class MyCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet var myTextField: UITextField!
weak var dataSourceDelegate: DataSourceUpdateDelegate?
func configureCellWithData(_ data: String?, delegate: DataSourceUpdateDelegate?)
{
myTextField.text = data
myTextField.delegate = self
dataSourceDelegate = delegate
}
override func prepareForReuse() {
myTextField.text = ""
super.prepareForReuse()
}
func textFieldDidEndEditing(_ textField: UITextField) {
dataSourceDelegate?.didUpdateDataIn(self, with: textField.text)
}
}
Make sure your View Controller conforms to DataSourceUpdateDelegate and initialize a variable to manage the data:
class MyViewController: UITableViewController, UITableViewDataSource, DataSourceUpdateDelegate {
var data = [IndexPath : String]()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyCell() // Dequeue your cell here instead of instantiating it like this
let cellData = data[indexPath]
cell.configureCellWithData(cellData, delegate: self)
return cell
}
func didUpdateDataIn(_ sender: UITableViewCell, with text: String?) {
data[tableView.indexPath(for: sender)!] = text
}
}
On my custom tableviewcell I have a button which the user can press which toggles between selected and unselected. This is just a simple UIButton, this button is contained with my customtableviewcell as an outlet.
import UIKit
class CustomCellAssessment: UITableViewCell {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var dateTaken: UILabel!
#IBOutlet weak var id: UILabel!
#IBOutlet weak var score: UILabel!
#IBOutlet weak var button: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func selectButton(_ sender: UIButton) {
if sender.isSelected{
sender.isSelected = false
}else{
sender.isSelected = true
}
}
}
The strange thing is, when I press a button on say the first cell it then selects a button 8 cells down on another cell (out of the view) in the same tableview. Each cell has its own button but it is as if using dequeReusableCell is causing the system to behave this way. Why is it doing this and is it to do with the way that UIbuttons work on tableviewcells?
Thanks
Each cell has its own button but it is as if using dequeReusableCell is causing the system to behave this way.
Wrong. UITableViewCells are reusable, so if your tableView has 8 cells visible, when loading the 9th, the cell nr 1 will be reused.
Solution: You need to keep track of the state of your cells. When the method cellForRowAtIndexPath is called, you need to configure the cell from scratch.
You could have in your ViewController an small array containing the state of the cells:
var cellsState: [CellState]
and store there the selected state for each indexPath. Then in the cellForRowAtIndexPath, you configure the cell with the state.
cell.selected = self.cellsState[indexPath.row].selected
So, an overview of I would do is:
1 - On the cellForRowAtIndexPath, I would set
cell.button.tag = indexPath.row
cell.selected = cellsState[indexPath.row].selected
2 - Move the IBAction to your ViewController or TableViewController
3 - When the click method is called, update the selected state of the cell
self.cellsState[sender.tag].selected = true
Remember to always configure the whole cell at cellForRowAtIndexPath
Edit:
import UIKit
struct CellState {
var selected:Bool
init(){
selected = false
}
}
class MyCell: UITableViewCell {
#IBOutlet weak var button: UIButton!
override func awakeFromNib() {
self.button.setTitleColor(.red, for: .selected)
self.button.setTitleColor(.black, for: .normal)
}
}
class ViewController: UIViewController, UITableViewDataSource {
var cellsState:[CellState] = []
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//Add state for 5 cells.
for _ in 0...5 {
self.cellsState.append(CellState())
}
}
#IBAction func didClick(_ sender: UIButton) {
self.cellsState[sender.tag].selected = true
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellsState.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MyCell
cell.button.isSelected = self.cellsState[indexPath.row].selected
cell.button.tag = indexPath.row
return cell
}
}
When you tap the button, you're setting the selected state of the button. When the cell gets reused, the button is still selected - you're not doing anything to reset or update that state.
If button selection is separate to table cell selection, then you'll need to keep track of the index path(s) of the selected buttons as part of your data model and then update the selected state of the button in tableView(_: cellForRowAt:).
A table view will only create as many cells as it needs to display a screen-and-a-bits worth of information. After that, cells are reused. If a cell scrolls off the top of the screen, the table view puts it in a reuse queue, then when it needs to display a new row at the bottom as you scroll, it pulls that out of the queue and gives it to you in tableView(_: cellForRowAt:).
The cell you get here will be exactly the same as the one you used 8 or so rows earlier, and it's up to you to configure it completely. You can do that in cellForRow, and you also have an opportunity in the cell itself by implementing prepareForReuse, which is called just before the cell is dequeued.
In your tableView(_: cellForRowAt:) take action for button click and add target
cell.button.addTarget(self, action: #selector(self.selectedButton), for: .touchUpInside)
At the target method use below lines to get indexPath
func selectedButton(sender: UIButton){
let hitPoint: CGPoint = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath: NSIndexPath = self.tableView.indexPathForRow(at: hitPoint)! as NSIndexPath
}
Then do your stuff by using that indexPath.
Actually your method can not find in which indexPath button is clicked that's why not working your desired button.
One way to approach your problem is as follow
1. First create a protocol to know when button is clicked
protocol MyCellDelegate: class {
func cellButtonClicked(_ indexPath: IndexPath)
}
2. Now in your cell class, you could do something like following
class MyCell: UITableViewCell {
var indexPath: IndexPath?
weak var cellButtonDelegate: MyCellDelegate?
func configureCell(with value: String, atIndexPath indexPath: IndexPath, selected: [IndexPath]) {
self.indexPath = indexPath //set the indexPath
self.textLabel?.text = value
if selected.contains(indexPath) {
//this one is selected so do the stuff
//here we will chnage only the background color
backgroundColor = .red
self.textLabel?.textColor = .white
} else {
//unselected
backgroundColor = .white
self.textLabel?.textColor = .red
}
}
#IBAction func buttonClicked(_ sender: UIButton) {
guard let delegate = cellButtonDelegate, let indexPath = indexPath else { return }
delegate.cellButtonClicked(indexPath)
}
}
3. Now in your controller. I'm using UItableViewController here
class TheTableViewController: UITableViewController, MyCellDelegate {
let cellData = ["cell1","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4"]
var selectedIndexpaths = [IndexPath]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellData.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 55.0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
cell.cellButtonDelegate = self
cell.configureCell(with: cellData[indexPath.row], atIndexPath: indexPath, selected: selectedIndexpaths)
return cell
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 30.0
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! MyCell
cell.buttonClicked(UIButton())
}
func cellButtonClicked(_ indexPath: IndexPath) {
if selectedIndexpaths.contains(indexPath) {
//this means cell has already selected state
//now we will toggle the state here from selected to unselected by removing indexPath
if let index = selectedIndexpaths.index(of: indexPath) {
selectedIndexpaths.remove(at: index)
}
} else {
//this is new selection so add it
selectedIndexpaths.append(indexPath)
}
tableView.reloadData()
}
}
I am new to iOS dev and basically I'm trying to populate a TableView with String values from an array.
However when I run the app, blank rows show up and no text values are shown. Have I coded this correctly?
import UIKit
class SelectIssueController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var issuesTableView: UITableView!
var issues = [Issue]()
override func viewDidLoad() {
super.viewDidLoad()
self.issues = ["Test1", "Test2", "Test3"]
self.issuesTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override var preferredStatusBarStyle: UIStatusBarStyle{
return UIStatusBarStyle.lightContent
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.issues.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.issuesTableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as UITableViewCell
cell.textLabel?.text = issues[indexPath.row]
//Even if I manually set a value, the rows are still blank
//cell.textLabel?.text = "Hello World"
return cell
}
}
You can set Table view data source and delegate in two ways.
1. Click Cntrl+drag from tableView to view controller. See below figure
Create the outlet of your tableView and assign its datasource and delegate in ViewDidLoad.
In your example you already have an outlet to issuesTableView, so you would write:
issuesTableView.dataSource = self
issuesTableView.delegate = self
Thanks:)
I have a UIViewController in which I've embedded UITableView. Because I don't want the UIViewController to get too heavy I separated the UITableViewDataSource and UITableViewDelegate
class ViewController: UIViewController {
#IBOutlet var tableView: UITableView!
var dataSource : UITableViewDataSource!
var tableDelegate: UITableViewDelegate!
override func viewDidLoad() {
super.viewDidLoad()
dataSource = TableViewDataSource()
tableDelegate = TableViewDelegate()
tableView.dataSource = dataSource
tableView.delegate = tableDelegate
// Do any additional setup after loading the view, typically from a nib.
}
}
class TableViewDataSource : NSObject, UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "MyCellIdentifier"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
cell.textLabel?.text = "hello"
cell.detailTextLabel?.text = "world"
return cell
}
}
class TableViewDelegate : NSObject, UITableViewDelegate {
//custom code here if required
}
In my Storyboard, I've created a prototype cell within the UITableView with the identifier
MyCellIdentifier
I use this identifier to create a cell in my UITableViewDataSource delegate method.
However, if I start the app, only the text of the left label is displayed. The detail label is not visible.
I looked into the debugger and noticed that detailLabel text is correct. The text of the right label is really "world". However, the label is not visible.
I've done a little bit of research and there has been a similar problem in the past. Whenever the text of the detailLabel was set to nil, it was not visible
However in my example, the text is not nil, it is set to "Detail":
How can I make the right label visible?
if (cell != nil)
{
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle,
reuseIdentifier: reuseIdentifier)
}
add this code under let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) this line of your code.