How to access index path of button in a custom TableViewCell? - ios

I have created a custom TableViewCell and currently have a button placed in the cell. When the button is pressed, In the tableviewcell.swift file, IBAction func gets executed. I cannot figure out how to determine the index path of the cell that the button is in that is pressed. I was trying to use the following
#IBAction func employeeAtLunch(sender: AnyObject) {
let indexPath = (self.superview as! UITableView).indexPathForCell(self)
println("indexPath?.row")
}
but I get the following error on click:
Could not cast value of type 'UITableViewWrapperView' to 'UITableView'
Any help on how to access the index path of the cell?

You are just assuming that the cell's immediate superview is the table view - wrongly. There is no particular reason why that should be so (and indeed it is not). Work with fewer assumptions! You need to keep walking up the superview chain until you do reach the table, like this:
var v : UIView = self
do { v = v.superview! } while !(v is UITableView)
Now v is the table view, and you can proceed to work out what row this is.
What I would actually do, however, is work my up, not from the cell to the table, but from the button to the cell. The technique is exactly the same:
var v : UIView = sender as! UIView
do { v = v.superview! } while !(v is UITableViewCell)
Do that the button's action method, where sender is the button. If the target of the action method is the table view controller, it has access to the table, and the problem is solved.

You could subclass UIButton in your cell with a property for its row.
class MyButton: UIButton {
var row: Int?
}
Then when you set up your table view, in the cellForRowAtIndexPath method, you set the row property:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// ...
cell.button.row = indexPath.row
// ...
}
This way when the action fires, you can get the correct row:
#IBAction func employeeAtLunch(sender: MyButton) {
if let row = sender.row {
// access the row
}
}

In your situation, I will add a tag to your button to identify in which row it is. Whenever I configure cell in the call-back cellForRowAtIndexPath, I update this tag value.
When a button clicked, the handler specifies always the button pressed. With the tag defined to that pressed button, you can know the button of which row is pressed.
#IBAction func buttonPressed(sender: AnyObject) {
//convert to UIButton
if let btn = sender as? UIButton {
let rowId = btn.tag
//do your works
}
}
If your tableview has more than 1 sections, you will have to setup the value of tags the right way.
The second solution which is better: get the position of the button in your tableView, then get indexpath of that position in your tableview:
let position = sender.convertPoint(CGPointZero, toView: self.tblMain)
let indexPath = self.tblMain.indexPathForRowAtPoint(position)

Related

Showing and hiding a view only on a specific cell of a table view

I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.

Segue from UITableViewCell by tapping on an image inside of cell

Been trying to figure this out for a while now and after a couple hours of searching for a solution I decided it was time to ask.
I have a tableview that gets populated by custom UITableViewCells and currently when you tap on a cell it takes you to a detail view.
Within the custom cell there is an image, I would like the user to be able to tap on that image and segue to a popover VC that shows the image.
What I'm having trouble with is creating the segue when the image is tapped.
In the file for the custom cell, I've set up a tap gesture recognizer on the image (pTap):
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(PostCell.voteTapped(_:)))
let ptap = UITapGestureRecognizer(target: self, action: #selector(PostCell.imageTapped(_:)))
tap.numberOfTapsRequired = 1
ptap.numberOfTapsRequired = 1
voteImage.addGestureRecognizer(tap)
voteImage.userInteractionEnabled = true
featuredImg.addGestureRecognizer(ptap)
featuredImg.userInteractionEnabled = true
}
I also have a function in the custom cell file for the tap:
func imageTapped(sender: UIGestureRecognizer) {
print("image tapped")
}
In my view controller file I've added a segue in did select row at index path:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let post: Post!
if inSearchMode {
post = filteredVenues[indexPath.row]
} else {
post = posts[indexPath.row]
}
print(post?.venueName)
performSegueWithIdentifier("imageTapped", sender: nil)
performSegueWithIdentifier("DetailsVC", sender: post)
}
Also, in the storyboard I have created a segue from the VC that holds the tableview with the custom cells to the VC I'd like to show when the image is tapped.
I've tried several different methods of getting this to work and haven't had any luck, the code you see above are what remains after my many failed attempts. I feel that the function for the tap in the custom cell file and the segue in the VC file are a part of the solution so that is why I have left them in.
Any help would be appreciated. Thanks!
Updates to code from answers below:
Added protocol
protocol ImageSegueProtocol: class {
func imageTapped(row: Int)
}
class PostCell: UITableViewCell {
Added IAB Func
#IBAction func imageTapped(sender: UIGestureRecognizer) {
guard let row = row else { return }
delegate?.imageTapped(row)
print("image tapped func")
}
Declared delegate in the Main VC
weak var delegate:postCell?
Assigned Delgate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//let post = posts[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as? PostCell {
var img: UIImage?
var vImg: UIImage?
postCell?.delegate = self
Added extension function
extension FeedVC: ImageSegueProtocol {
func imageTapped(row: Int) {
if inSearchMode == true {
let object = filteredVenues[row]
performSegueWithIdentifier("imageTapped", sender: object)
print("ext func")
} else {
let object = posts[row]
performSegueWithIdentifier("imageTapped", sender: object)
print("ext func")
}
}
You could do it like this:
Within the cell file, declare a protocol at the top, and then set up some properties within the cell class itself and the delegate behaviour in response to the image being tapped:
protocol SomeCellProtocol: class {
func imageTapped(row: Int)
}
class SomeCell: UITableViewCell {
weak var delegate: SomeCellProtocol?
var row: Int?
#IBAction func imageTapped() {
guard let row = row else { return }
delegate?.imageTapped(row)
}
}
You must then make your ViewController adopt SomeCellProtocol and call the segue within like so:
extension SomeViewController: SomeCellProtocol {
func imageTapped(row: Int) {
//get object from array and call segue here
}
}
And you must set your ViewController as the delegate of your cells by calling:
someCell.delegate = self
and pass the row to the cell:
someCell.row = indexPath.row
within your cellForRowAtIndexPath method of the ViewController.
So, when the button is tapped within the cell (or you can do it with a GestureRecognizer on the ImageView if you want) it will force the delegate (the ViewController) to call its imageTapped function, passing a row parameter which you can use to determine which object in the table (its corresponding data array) should be passed via the Segue.
As OhadM said, create a button and set the background image as the image you want displayed. From there, you don't even need an IBAction. Control-drag from the button to the next view controller and create the segue.
Then, if you want to do any setup before the segue, in the first VC you'd have something like:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "imageButtonPressed" { // Make sure you name the segue to match
let controller = segue.destinationViewController as! SecondVC
controller.someText = "Hi"
controller.someInt = 5
}
}
What you need to do is to create a button instead of the image and this button will hold the actual image:
The rest is easy, ctrl drag an IBAction into your custom cell file.
Now you need to communicate with your View Controller in order to invoke your segue method.
You can achieve that using 2 design patterns:
Using post notification:
Inside your View Controller add an observer:
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(YourViewController.cellSelected(_:)),
name: "CellSelectedNotificationName",
object: nil)
Your method cellSelected inside your View Controller should look like this:
func cellSelected(notification : NSNotification)
{
if let passedObject = notification.object as? YourCustomObject
{
// Do what ever you need with your passed object
// When you're done, you can invoke performeSegue that will call prepareForSegue
// When invoking performeSegue you can pass your custom object
}
}
Inside your CustomCell class at the IBAction method of your button:
#IBAction func buttonTapped()
{
// Prepare the object you want to pass...
NSNotificationCenter.defaultCenter().postNotificationName("CellSelectedNotificationName", object: YourCustomObjectYouWantToPass)
}
In a nut shell, a button inside a cell was tapped, you created an object inside that cell and you pass it to a View Controller using the notification centre. Now, you can segue with your custom object.
NOTE: If you don't need to pass an object you can basically ctrl + drag from your UIButton (at your storyboard) to another View Controller.
Using a delegate that is pointing to the View Controller.
Good luck.

Swift - How to get row data from custom UITableViewCell on UIImageView click from within the cell

I’m trying to get data associated with the UITableViewCell where a UIImageView is clicked within the cell. The code I currently have to capture the click event is working fine. However, once I get into the click function that is called, I’m unable to retrieve the associated data from the same UITableViewCell where the UIImageView was clicked. Here is the code I'm using to set up the click event. This code is contained in cellForRowAtIndexPath:
var tap = UITapGestureRecognizer(target: self, action: Selector("staticMap_click:"))
cell.imgStaticMap.addGestureRecognizer(tap)
cell.imgStaticMap.userInteractionEnabled = true
Here is the function staticMap_click that gets called when the UIImageView is clicked:
func staticMap_click(sender:UITapGestureRecognizer)
{
let rowData: NSDictionary = self.arrayPosts[sender.valueForKey("row") as Int] as NSDictionary
mdblStaticLat = rowData["dblLat"] as String
mdblStaticLong = rowData["dblLong"] as String
self.performSegueWithIdentifier("sgShowMapFromStaticClick", sender: self)
}
As you can see, I'm unsure of how to reference the data for the row that was clicked. I attempted setting a tag on the UIImageView, but that didn’t work. I also attempted to set a tag on the UITapGestureRecognizer, but I haven’t been able to get that to work either.
Does anyone know how I can reference the data from the selected row where the UIImageView is tapped?
You might be interested in
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
}
This function will be called whenever a cell is pressed.
You can access the row which is pressed by using indexPath.row
Furthermore you can use
func staticMap_click(sender:UITapGestureRecognizer){
let tappedView = sender.view as? UIImageView
let indexPath = (tappedView.superview as UITableView).indexPathForCell(self)
}
To get the cell which was tapped if you want only the Image to be tapable
This code will also return an indexPath, you will be able to use indexPath.row again to get the row.
Getting the row will enable you to get data out of an array if deemed necessary

Detect UIButton while using custom UITableViewCell

I want to implement a custom UITableViewCell class that is made up of subviews such as, labels, buttons, and images. These cells will display content fetched from the web using an API.
I do not want to implement the UITableView delegate method didSelectRowAtIndexPath as this will make the entire cell selectable. Only the button in the cell should be able to trigger any action.
The button is connected from the storyboard to the custom UITableViewCell via IBOutlet. The addTarget(_:action:forControlEvents:) method is called on the UIButton in cellForRowAtIndexPath of the UITableViewController class.
The roadblock occurs when we want to detect the indexPath of the selected cell in the Selector function of the button.
This is how the indexPath for the selected cell can be detected in the Selector function
#IBAction func doSomething(sender: AnyObject) {
var location: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
var indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(location)!
println("The indexPath for Selected Cell is - \(indexPath.row)")
}
Although, this successfully gets me around the issue, my question is;
1) Have you found an alternate way of being able to use UIButtons to pass selected cell data in a custom UITableViewCell?
2) What would be the best practice, so far, in Swift to implement a similar scenario?
One way would be to iterate over the superviews of the sender and see if a UITableViewCell can be found...
Let's say you created a generic UIView extension with a method that would check to see if any of its superview's was a UITableViewCell...
extension UIView {
func parentTableViewCell() -> UITableViewCell? {
var view = self
while let superview = view.superview {
if let cell = superview as? UITableViewCell {
return cell
} else {
view = superview
}
}
return nil
}
}
Then you could do the following...
if let cell = (sender as UIView).parentTableViewCell() {
let indexPath = tableView.indexPathForCell(cell)
println("The row for this cell is - \(indexPath.row)")
}
Another way would be to use the tag property of a view by setting it to an Int and then check to see what the tag of the sender was in the method.
i.e. In your tableView:cellForRowAtIndexPath: method
cell.myButton.tag = indexPath.row
Then in your doSomething method
let rowIndex = (sender as UIButton).tag
println("The row for this cell is - \(rowIndex)"
If your tableView only has one section, you can use the tag property of the button to store the indexPath.row.
In cellForRowAtIndexPath, when you set the target-action for the button, set button.tag = indexPath.row.
Then in your doSomething routine:
#IBAction doSomething(sender: UIButton) {
println("This button is from row - \(sender.tag)")
}

UILabel Not Populated From Array When Button Pressed

I have a UITableViewController with custom UITableViewCells which contain a UILabel and a UIButton.
The problem is that the UILabel which is initially set to 0, is not changing when the button is pressed which changes its particular value in the array. When I use println() the actual array has been changed, but the UILabel has not.
The following is the code from my UITableViewController:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let groupChatCell = tableView.dequeueReusableCellWithIdentifier("groupChatCell", forIndexPath: indexPath) as textCell
groupChatCell.voteScoreLabel.text="\(groupVote[indexPath.row])"
groupChatCell.voteScoreLabel.tag=indexPath.row
return groupChatCell
}
voteScoreLabel is the UILabel which I want to update from the array.
groupVote is the array that is meant to populate the label with the scores. I set the UILabel tag equal to its index path so that I can then reference it when I need to pick out a particular value of the array.
The following is from my custom UITableViewCellController:
#IBAction func upVoteButtonPressed(sender: AnyObject) {
groupVote[voteScoreLabel.tag]=groupVote[voteScoreLabel.tag]+1
}
What am I doing wrong here so that I can make the UILabel change when its corresponding value in the array has changed.
You need to reload the your tableView data when a button is clicked.
The reloadData() function calls every tableView delegate/data source method again and reload the whole controller if its UITableViewController or the whole tableView if its a view added to another controller
Your code should look like this:
#IBAction func upVoteButtonPressed(sender: AnyObject) {
groupVote[voteScoreLabel.tag]=groupVote[voteScoreLabel.tag]+1
instance_of_your_tableView.reloadData()
// If this is the name of method in swift //
}
Regards

Resources