How to pass custom UITableViewCell in a selector - ios

I have a custom UITableViewCell that contains a UISwitch and a UILabel. When the switch has been tapped, I would like to know which cell has had their switch activated/deactivated.
I have the following code in my cellForRowAtIndexPathMethod to add the target to my cell's UISwitch:
[cell.notifSwitch addTarget:self action:#selector(switchChangedFromCell:) forControlEvents:UIControlEventValueChanged];
Here is the code for the selector:
- (void)switchChangedFromCell:(id)sender {
HydrationNotificationTableViewCell *cell = (HydrationNotificationTableViewCell *)sender;
if ([cell.notifSwitch isOn]) {
NSLog(#"Notification for %# has been activated", cell.notifLabel.text);
}
else {
NSLog(#"Deactivated notification %#", cell.notifLabel.text);
}
}
Right off the bat I believe that it is wrong of me to cast the sender as a cell, since the sender really is the UISwitch from the cell. I would like to know how I can pass the cell itself as well so I know which cell has been changed.
One solution is to set a different tag for each UISwitch in my cellForRowAtIndexPath method, but I was wondering if there was a cleaner solution.

The bad news is that you can't do that... exactly.
The good news is that in your IBAction the UISwitch is the sender, and you can use that to figure out which cell contains the UISwitch.
I have a project on Github called TableViewExtension that shows how to do it.
The only real trick is this extension to UITableView:
public extension UITableView {
/**
This method returns the indexPath of the cell that contains the specified view
- Parameter view: The view to find.
- Returns: The indexPath of the cell containing the view, or nil if it can't be found
*/
func indexPathForView(_ view: UIView) -> IndexPath? {
let origin = view.bounds.origin
let viewOrigin = self.convert(origin, from: view)
let indexPath = self.indexPathForRow(at: viewOrigin)
return indexPath
}
}
If you add that extension you can then make your IBAction use the sender to figure out which control triggered the tap, and call that method to figure out which cell contains that control:
#IBAction func controllTriggered(_ sender: UISwitch) {
guard let indexPath = tableView.indexPathForView(sender) else { return }
//Now you have the indexPath of the cell containing `sender`
}
Edit:
Note that in your code you are trying to get the cell, and then query the different subviews of the cell to get state data. Don't do that. You should be storing your state data in your model object. The cell is for displaying information and interacting with the user.
If the user taps a switch the IBAction will have the switch as it's sender parameter. You can get the value of the Switch from sender.

I believe you can only pass the sender in a selector method like that.
You could consider having the UISwitch method be in your custom UITableViewCell class, and create a delegate protocol for your custom class that passes the cell to your ViewController.

I came up with another solution but this is also a bit ugly. I casted the cell to the sender's superview's superview (UISwitch >> Content View >> Cell)
HydrationNotificationTableViewCell *cell = (HydrationNotificationTableViewCell *)([senderSwitch superview].superview);
Hopefully there's a cleaner solution out there.

Related

How to save the text in textfield in swift?

I have a tableview consisting of 2 cells and each cell has a textfield. I'd like to hold the text in textfield in an another variable and append it into an array but it just save the second cell's textfield's text into the variable and doesn't append the array.
Here is cellForRowAt code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == queryTableView {
let cell = tableView.dequeueReusableCell(withIdentifier: queryCellReuseIdentifier,
for: indexPath) as! queryCell
cell.selectionStyle = UITableViewCell.SelectionStyle.gray
queryTextField = UITextField(frame: CGRect(x: 20, y: 0, width: 300, height: 20))
queryTextField.delegate = self
queryTextField.placeholder = "Soruyu buraya giriniz"
queryTextField.font = UIFont.systemFont(ofSize: 15)
cell.addSubview(queryTextField)
return cell
}
Here is my related function :
func textFieldDidEndEditing(_ textField: UITextField) {
var temp = queryTextField.text!
queryArray.append(temp)
print(temp)
}
if your project is non-storyboard approach, then I would say, it would be more easier and flexible to take control of each type and data and action since sometimes things not quite flexible with storyboard only.. . as you create, even a single UIElement, UI conforms to those properties, methods and actions which you define, means fully customized code - custom defined code base..
now to your question, according to what you did, probably, your textfield text is overwritten to blank, losing the previously entered value every time the cell is dequeued, cell is generated fresh again for use every time " dequeue cell " method is executed, if you want to persist your previous value, I would say, " remind the dequeued cell that it had some value before being re- dequeued again " means:
declare your global array or dict.
dequeue your cell and setup your cell properties
3 after cell is dequeued, do something like this, and your cells previous value is saved even after the cell is dequeued.
example, common approach:
struct DefaultInfo {
var string: String?
var type: SomeData? //optional
}
let array: [DefaultInfo] = []
var array = self.array[indexPath.row]
// after cell is dequeued:
switch array.type {
case .someCase:
let string = cell.textField.text
array.string = string
cell.textField.text = array.string
//print(array.string!)
// now you should be able to see your input after cell is
//renewed.. . try it.. .
default: break
}
It seems like you have an independant variable queryTextField which is overriden when you create the second cell.
Also, in textFieldDidEndEditing try accessing textField instead of queryTextField, like this:
func textFieldDidEndEditing(_ textField: UITextField) {
var temp = textField.text!
queryArray.append(temp)
print(temp)
}
Responding to this:
I need every cell has its textfield and I need to be able to save
their texts into an array. After that I am going to send them to web
service. To sum up, they will be my parameters.
You don't need to have global queryTextField just for that. You can remove this variable, if that's its only goal.
Do you need to send web request on some trigger? Like button tap. I assume, yes.
Since theoretically not all your cells are visible at the same time (e.g. when thay do not fit the screen), it's bad idea to try and track texts in text fields. Instead, you need some kind of model to store all texts (paired with indeces, maybe). The simplest would be dictionary where key is cell identifier (e.g. indexPath) and value is the text.
In textFieldDidEndEditing you can report the changes to the view controller. For this you need to assign the cell as the delegate for its text field. And view controller - as the delegate for the cell. In textFieldDidEndEditing cell would be calling view controller delegate method to report the text change, passing (for example) self and text as parameters. View controller would then be able to find its index path and store the text in the model (dictionary).
On trigger (button click?) view controller will be able to build parameters from the model (dictionary).
Consider this pseudocode as a direction:
cellForRow {
...
cell = ...
let textField = ...
textField.delegate = cell
cell.addSubview(textField)
cell.delegate = self
}
In cell:
textFieldDidEndEditing(textField: UITextField) {
delegate?.report(text: textField.text ?? "", in: self)
}
In view controller:
report(text: String, in cell: UITableViewCell) {
let indexPath = tableView.indexPath(of: cell)
model[indexPath] = text
}

Proper way to get indexes of a UITableViewCell in response to a button click which sits in a tableview within tableview?

Screenshot of the app:
I have an 'x' button to delete a TableViewCell which is in a table within a table. On click of the button, I would like to remove the cell, so I need to know the 2 indexes of the button click, the row if the first table view, and then within that tableview the row of the cell which the button was clicked. All I have is the sender.
So to be a bit clearer, in the screenshot, if someone clicks the x under the ford fiesta, I need to get indexpath 0 for the "subtableview" and 1 for the tableview, and that way I know to delete this element from the table datasource.
I do it successfully by doing:
var cell = sender.superview
while (cell != nil) && !((cell?.isKind(of: CustomCell.self))!) {
cell = cell?.superview
}
let tbl = cell?.superview as! UITableView
let indexPath = tbl.indexPath(for: (cell as? UITableViewCell)!
)
The stupid thing is I have to do it twice, once to find the index of the cell within the "sub"tableview, and then again to find the index of the "subtableview" within the tableview.
Is there a better way to do this? Isnt there a way to get the buttonClick to get the didSelectRowAt to fire and add the sender object to it (so I know that a button was clicked as opposed to the cell being selected)?
EDIT I forgot to mention that the first tableview opens and closes on click, so the main tableview has 2 different cell types, one closed (so no nested tableview) and then onselect of a row from that tableview, the cell is replaced with a detailed cell which has another tableview inside it, thats why sectioned tableview isnt a solution (to the best of my knowledge, I'm new here)
One way to do it is to use closures. You set up your cell with a closure and then call it. Pretty much like this:
class CellWithClosure: UITableViewCell {
var button: UIButton = UIButton()
var closureForButton: (Void) -> Void
func setupCell(closureForButton: #escaping (Void) -> Void) {
self.closureForButton = closureForButton
button.addTarget(self, action: #selector(buttonAction), for: UIControlEvents.touchUpInside)
}
#objc func buttonAction() {
closureForButton()
}
}

Is it possible to derive the indexPath if you know the row?

Okay, I'm going to try to break this down as simply as I am able. I have a tableView in a ViewController. I have two prototype cells for the table. I am reusing the cells multiple times to populate the table.
In one of the cells, I've added gesture recognizer to the label through which I'm making a textField visible on place of the label and hiding the label. Now I want the labels text to change to what I've entered in the textField when I'm done using the textField and hit the return key. So i implemented the UITextFieldDelegate protocol in the viewController. I've also added tags to each of the textFields in the cell so that I know what textField is returning and what row the textField is in.
Basically, what I want to know is if there is any way to get the indexPath if I already know the indexPath.row?
For the gesture recognizers, i was able to work around this issue by getting the indexPath from the tapped location:
func genderTapped(sender: UITapGestureRecognizer) {
let tapLocation = sender.locationInView(self.profileInfoTable)
let indexPath = self.profileInfoTable.indexPathForRowAtPoint(tapLocation)
let cell = self.profileInfoTable.cellForRowAtIndexPath(indexPath!) as! editUserDataCell
cell.savedUserInput.hidden = true
cell.userDetailTextfield.becomeFirstResponder()
cell.userDetailTextfield.hidden = false
cell.userDetailTextfield.text = cell.savedUserInput.text!
}
I need the indexPath so that I can refer to the elements contained within a cell. Can anyone offer any insights? Has anybody tried a similar approach? Is there any way I can access the cell by just using the row?
If you are able to get the indexPath inside the GestureMethod then you can create one instance property of type NSIndexPath store its value inside that Gesture's method and later used the indexPath inside textFieldShouldReturn delegate method, something like this.
var selectedIndexPath: NSIndexPath?
func genderTapped(sender: UITapGestureRecognizer) {
let tapLocation = sender.locationInView(self.profileInfoTable)
self.selectedIndexPath = self.profileInfoTable.indexPathForRowAtPoint(tapLocation)
let cell = self.profileInfoTable.cellForRowAtIndexPath(self.selectedIndexPath!) as! editUserDataCell
cell.savedUserInput.hidden = true
cell.userDetailTextfield.becomeFirstResponder()
cell.userDetailTextfield.hidden = false
cell.userDetailTextfield.text = cell.savedUserInput.text!
}
Now use this self.selectedIndexPath inside UITextFieldDelegate method.
Edit: From your question's comment you have told that you have just one Section so you can also create indexPath from that textField's tag this way.
func textFieldShouldReturn(textField: UITextField) -> Bool {
let indexPath = NSIndexPath(forRow: textField.tag, inSection: 0)
//Or You can use self.selectedIndexPath also
}
In case of single or multiple sections, the below code will work
In your cellForRowAtIndexPath, set the tag as below:-
let tag = indexPath.section*100 + indexPath.row
cell.savedUserInput.tag = tag
cell.userDetailTextfield.tag = tag
In your textfield delegate method, get the indexPath as follows:-
func genderTapped(sender: UITapGestureRecognizer) {
let textfieldObject = sender as! UITextField
let sectionTag = textfieldObject.tag % 100
let rowTag = textfieldObject.tag / 100
let indexPath = NSIndexPath(forRow: rowTag.tag, inSection: sectionTag)
}
Disclaimer: This is not an answer to the literal question asked here, but it might provide an simpler solution to OP's goal.
Unless you need to do something in addition to what you described in your question it seems to me that a much easier solution would be not to use labels at all but in stead just use an UITextField and set it's enabled property to false when you want it to act like an label.
You can subclass the UITextField if you need the styling to change when the mode changes.
If you know the row number which you are accessing and the section in which the row is, then use this code
let indexPath = NSIndexPath(forRow: row, inSection: section)
For accessing the cell corresponding to this indexPath, use
let cell = tableView.cellForRowAtIndexPath(indexPath) as! tableViewCell

How to save changed state of dynamic created UISwitch in CoreData model in Swift?

I have a UITableView containing dynamic cells added by user. Each cell has one UILabel and one UISwitch. The default state of switch is ON. I save these values in a CoreData model (UILabel as String & UISwitch as Bool) whenever user adds it.
I would like to update/store the state of switch corresponding to value of label in the same cell in CoreData model whenever user toggles the switch. I have added tags (switch.tag = indexPath.row) to each switch in row and can access it by using sender.tag in my cellChanged function. But I am not able to fetch label of that row in which switch has been toggled. If I can somehow fetch value of label corresponding to the switch toggled then I can run a for loop to look for that label value in CoreData model and then subsequently set state of UISwitch. Since I am using Swift language, I will appreciate if you can explain code using the same.
This worked for me:
func switchTapped(sender:UISwitch) {
let switchPosition = sender.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(switchPosition)
}
A better pattern can be creating a protocol in cell. something like
#protocol MyCellDelegate{
func cellSwitchChanged(forCell: MyCell, toValue: Bool, withLabel: String?)
}
class MyCell: UITableVeiwCell{
weak veer delegate: MyCellDelegate
.
.
.
#IBaction handleSwitchChanged(sender: UISwitch){
self.delegate?.cellSwitchChanged(forCell: self, toValue: sender.value, withLabel: self.lblName.text)
}
}
And in you cell for row an indexPath, assign you vc to be delegate of the cell. and override this method.
So every time the switch changes, the view controller will get the cell reference, switch value and the text label. you can then do your core data update operation here.

UITextField.becomeFirstResponder() does not work properly

I'm writing a to do list app, and when I click a UIButton to add a new task, I want a new TableViewCell to pop up, that contains a UITextField. However, when I set this UITextfield to first responder, with the following code:
let cellIdentifier = "TaskTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: newIndexPath) as! TaskTableViewCell
cell.nameLabel.becomeFirstResponder()
However when I do this, a keyboard pops up but a cursor does not, and when I type, I can see autocorrect options, but no characters ever appear in the textfield.
Wondering what's going on? Thanks!
You should create IBOutlet for textField and IBAction for button. And it should look like this:
class TaskTableViewCell: UITableViewCell
{
#IBAction func buttonPressed()
{
self.myTextField.becomeFirstResponder()
}
}

Resources