I display a shopping list for different product groups as a table view with multiple sections. I want to add items with an add button for each group. So I equipped the header cell with a UIToolbar and a + symbol as a UIBarButtonItem.
Now every product group has an add button of his own:
If one add button was pressed, I have a problem identifying which one was pressed.
If I connect the add button with a seque, the function prepareForSeque(...) delivers a sender of type UIBarButtomItem, but there is no connection to the header cell from were the event was triggered.
If I connect an IBAction to the UITableViewController, the received sender is also of type UIBarButtomItem, and there is no connection to the header cell, too.
If I connect an IBAction to my CustomHeaderCell:UITableViewCell class, I am able to identify the right header cell.
This line of code returns the header cell title:
if let produktTyp = (sender.target! as! CustomHeaderCell).headerLabel.text
However, now the CustomHeaderCell class has the information I need.
But this information should be available in the UITableViewController.
I couldn't find a way to feed the information back to the UITableViewController.
import UIKit
class CustomHeaderCell: UITableViewCell {
#IBOutlet var headerLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
#IBAction func neuesProdukt(sender: UIBarButtonItem) {
if let produktTyp = (sender.target! as! CustomHeaderCell).headerLabel.text
{
print(produktTyp)
}
}
}
Here's how I typically handle this:
Use a Closure to capture the action of the item being pressed
class CustomHeaderCell: UITableViewCell {
#IBOutlet var headerLabel: UILabel!
var delegate: (() -> Void)?
#IBAction func buttonPressed(sender: AnyObject) {
delegate?()
}
}
When the Cell is created, create a closure that captures either the Index Path or the appropriate Section.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let section = indexPath.section
let cell = createCell(indexPath)
cell.delegate = { [weak self] section in
self?.presentAlertView(forSection: section)
}
}
Related
I want to implement UITableView Where I want to have 3 buttons in each UITableViewCell. I want to perform a diffeent action for each button. How can I identify which button is bring pressed and then get the object(index row ) of the cell that was selected?
UIViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let show=shows[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ShowCell") as!
ShowCell
cell.setShow(show: show)
return cell
}
UITableViewCell
#IBOutlet weak var graphButton: FlatButton!
#IBOutlet weak var buyButton: FlatButton!
#IBOutlet weak var reviewButton: FlatButton!
func setShow(show :StubHubEvent ){
let url = URL(string: show.imageurl)!
showImageView.af_setImage(withURL: url)
showImageView.contentMode = .scaleAspectFill
showImageView.clipsToBounds = true
nameLabel.text = show.title
dateLabel.text = show.time
implement your button action in UIviewcontroller not a UITableViewCell, create the target in inside the cellforRow as well as add the Tag for each target for identify which button was user pressed.for E.g
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let show=shows[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ShowCell") as!
ShowCell
cell.graphButton.tag = indexPath.row
cell.buyButton.tag = indexPath.row
cell.reviewButton.tag = indexPath.row
cell.graphButton?.addTarget(self, action: #selector(self.graphButtonClicked(_:)), for: .touchUpInside)
cell.buyButton?.addTarget(self, action: #selector(self.buyButtonClicked(_:)), for: .touchUpInside)
cell.reviewButton?.addTarget(self, action: #selector(self.reviewButtonClicked(_:)), for: .touchUpInside)
cell.setShow(show: show)
return cell
}
and handle the action as like
#objc func buyButton( _ sender: UIButton) {
print("buyButton Action Found the index of \(sender.tag)")
}
#objc func graphButtonClicked( _ sender: UIButton) {
print("graphButtonClicked Action Found the index of \(sender.tag)")
}
#objc func reviewButtonClicked( _ sender: UIButton) {
print("reviewButtonClicked Action Found the index of \(sender.tag)")
}
Option 2
if you want to perform in your button action in UItableviewcell class using delegate pattern, then refer this duplicate answer
there are two ways to get button click execution in the ViewController from TableViewCell
Use Delegate pattern
Use blocks as callbacks and handle block execution in the cellForRow method
Add addTarget(:) to add a target method for the button click
Details:
The first approach is best among all the three mentioned approaches, in this, you need to create a delegate which redirects your user actions from cell to view controller. Check below code example.
The second approach is similar to the first one, it just redirects the same method calls using the blocks instead of Delegate methods and protocol.
The third approach is not good, as it is tightly coupled with the indexPath.row value, in the software development industry
Cohesion should be high, Coupling should be low.
Code of first Approach:
//MARK:- Model - StubHubEvent
class StubHubEvent {
//you model class implementation
}
//MARK:- Protocol - ShowCellUIInteractionDelegate - used to redirect user actions from cell to viewController
protocol ShowCellUIInteractionDelegate: AnyObject {
func showCell(cell: ShowCell, didTapBuyFor show: StubHubEvent)
func showCell(cell: ShowCell, didTapGraphFor show: StubHubEvent)
func showCell(cell: ShowCell, didTapReviewFor show: StubHubEvent)
}
//MARK:- Cell- ShowCell
class ShowCell: UITableViewCell {
var show: StubHubEvent!
weak var delegateUIInteraction: ShowCellUIInteractionDelegate?
func setShow(show :StubHubEvent ){
self.show = show
//your other setup
}
//Bind these three action from cell to buttons as a .touchUpInside event
#IBAction func buttonBuyDidTap( _ sender: UIButton) {
self.delegateUIInteraction?.showCell(cell: self, didTapBuyFor: self.show)
}
#IBAction func buttonGraphDidTap( _ sender: UIButton) {
self.delegateUIInteraction?.showCell(cell: self, didTapGraphFor: self.show)
}
#IBAction func buttonReviewDidTap( _ sender: UIButton) {
self.delegateUIInteraction?.showCell(cell: self, didTapReviewFor: self.show)
}
}
//MARK:- ViewController - ShowListingViewController
class ShowListingViewController: UIViewController {
//you ShowListingViewController implementation
}
//MARK:- Extension - ShowCellUIInteractionDelegate
extension ShowListingViewController: ShowCellUIInteractionDelegate {
//execute your logic for the show model object
func showCell(cell: ShowCell, didTapBuyFor show: StubHubEvent){
}
func showCell(cell: ShowCell, didTapGraphFor show: StubHubEvent){
}
func showCell(cell: ShowCell, didTapReviewFor show: StubHubEvent){
}
}
I've a custom UITableViewCell, in that I've two UILabels & one UIButton. I'm able to load data...and display it as per requirement.
Problem Statement-1: Now problem exist in my UIButton, which is in my UICustomTableViewCell. Due to this I'm unable to handle click event on that UIButton.
Problem Statement-2: On button Click I have to identify the index of that Button click and pass data to next ViewController using segue.
Now have a look on...what did I've tried for this...
Yes, first-of-all I have thought that Binding IBOutlet action in my CustomCell will resolve my problem...but actually it doesn't solved my problem.
After that I've accessed button using .tag and initialised index path.row to it.
But it won't helped me.
So now I'm using Protocol oriented concept using delegate to handle click event on my UIButton which is available in CustomCell.
What did I tried:
SwiftyTableViewCellDelegate:
protocol SwiftyTableViewCellDelegate : class {
func btnAuditTrailDidTapButton(_ sender: LeadCustomTableViewCell)
}
CustomTableViewCell with delegate:
class LeadCustomTableViewCell: UITableViewCell {
#IBOutlet weak var lblMeetingPersonName: UILabel!
#IBOutlet weak var lblPolicyNo: UILabel!
#IBOutlet weak var btnLeadAuditTrail: UIButton!
weak var delegate: SwiftyTableViewCellDelegate?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
#IBAction func btnAuditTrailTapped(_ sender: UIButton) {
delegate?.btnAuditTrailDidTapButton(self)
}
}
ViewController implementing delegate:
class LeadViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SwiftyTableViewCellDelegate {
//IBOutlet Connections - for UITableView
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//setting dataSource & delegates of UITableView with this ViewController
self.tableView.dataSource = self
self.tableView.delegate = self
//Reloading tableview with updated data
self.tableView.reloadData()
//Removing extra empty cells from UITableView
self.tableView.tableFooterView = UIView()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:LeadCustomTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "cell") as! LeadCustomTableViewCell
//Assigning respective array to its associated label
cell.lblMeetingPersonName.text = (meetingPersonNameArray[indexPath.section] )
cell.lblPolicyNo.text = (String(policyNoArray[indexPath.section]))
cell.btnLeadAuditTrail.tag = indexPath.section
cell.delegate = self
return cell
}
//This is delegate function to handle buttonClick event
func btnAuditTrailDidTapButton(_ sender: LeadCustomTableViewCell) {
guard let tappedIndexPath = tableView.indexPath(for: sender) else { return }
print("AuditTrailButtonClick", sender, tappedIndexPath)
}
Don't know why this is not working.
Link the touch up inside event in cellForRow by adding the following code:
cell.btnLeadAuditTrail.addTarget(self, action:#selector(btnAuditTrailDidTapButton(_:)), for: .touchUpInside)
In tableview, each cell has a bookmark image button. When tapping on it, it becomes in a selected state and the image (red bookmark icon) is shown. I have delegate method to add information in cells with selected buttons to an array:
protocol CustomPoetCellDelegate {
func cell(_ cell: CustomPoetCell, didTabFavIconFor button: UIButton)
}
class CustomPoetCell: UITableViewCell {
#IBOutlet weak var poetNameLabel: UILabel!
#IBOutlet weak var verseCountLabel: UILabel!
#IBOutlet weak var periodLabel: UILabel!
#IBOutlet weak var iconButton: UIButton!
var delegate: CustomPoetCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
iconButton.setImage(UIImage(named: "icons8-bookmark-50-red.png"), for: .selected)
iconButton.setImage(UIImage(named: "icons8-bookmark-50.png"), for: .normal)
iconButton.isSelected = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func favIconTapped(_ sender: UIButton) {
delegate?.cell(self, didTabFavIconFor: sender)
}
}
In tableViewController:
func cell(_ cell: CustomPoetCell, didTabFavIconFor button: UIButton) {
if !button.isSelected {
let indexPathRow = tableView.indexPath(for: cell)!.row
if isFiltering() {
favoritePoets.append(searchResults[indexPathRow])
button.isSelected = true
} else {
favoritePoets.append(poets[indexPathRow])
button.isSelected = true
}
} else {
button.isSelected = false
favoritePoets = favoritePoets.filter { $0.arabicName != cell.poetNameLabel.text}
}
}
isFiltering() method checks if searchBar is in process.
Now, if isFiltering() is false, everything is fine, but if isFiltering() is true (that is, searching for a name in tableView), when I tap on a specific cell button to select it and then click cancel to dismiss the searchBar, icons for other cells are also selected, in spite of that only the one I tapped is added to the array. When navigating back and forth from the view, wrong selections are gone and only the right one is selected.
Any idea on what's going on?
Thanks in advance.
The problem is that the UITableView re-uses cell for optimized scroll performance. Hence it re-uses the cell which is not in view anymore with a new cell about to be displayed. Therefore you need to set the state of the cell when ever it is updated/reused.
The following function is called everytime. So you need to set cell properties for selected or non-selected state over here.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.indexPath = indexPath //Create an indexPath property in your cell and set it here
//This will be required when you call the delegate method
//configure your cell state
return cell
}
You can update your models in favoritePoets array by changing the specific property in delegate method that you are already calling for e.g
favoritePoets[indexPath.row].selected = true or false
For accessing the indexPath you need to set it as mentioned in above code. And then pass it as an argument in your delegate method. Now this updated property will help you set your state in cellForRowAt function.
Hope it helps :). Feel free to comment.
I have a custom dynamic table view cell with a label that has a tap gesture recognized added. When the user taps the label, not anywhere else in the cell, I want to present a view controller.
The instagram app has this feature. Ie. when you tap likes, it takes you to a likes table view, when you tap comments, it shows you to a comments table view. This is the same experience I want.
I am not looking to use didSelectRow because then it kind of defeats the purpose of having the specific target area to tap to show a new view controller.
So, how can I present a view controller from a tap gesture recognizer in a subclass of UITableViewCell?
UPDATED:
I am passing a closure to my custom TableViewCell which is successfully being called when the button is pressed. But I am stuck in the TableView and cannot pass information to the next View Controller I want to present. And I can't actaully perform the segue either :\
// From UITableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let story = stories[indexPath.row]
if let cell = tableView.dequeueReusableCell(withIdentifier: "Fun Cell", for: indexPath) as? FunTableViewCell {
cell.configureCell(title: story.title, info: story.info)
cell.buttonAction = { [weak self] (cell) in
print("the button was pressed for \(story.title)")
self?.buttonWAsTapped(title: story.title)
}
return cell
} else {
return FunTableViewCell()
}
}
func buttonWAsTapped(title: String) {
// Need to pass something to the next View Controller... but how???
if let nextVC = UIViewController() as? DetailViewController {
nextVC.storyTitle = title
performSegue(withIdentifier: "Button Pressed", sender: self)
}
}
// Custom TableViewCell
class FunTableViewCell: UITableViewCell {
#IBOutlet weak var funLabel: FunLabel!
#IBOutlet weak var standardLabel: TappedLabel!
#IBOutlet weak var funButton: FunButton!
var buttonAction: ((UITableViewCell) -> Void)?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(readMoreTapped))
tap.numberOfTapsRequired = 1
standardLabel.addGestureRecognizer(tap)
standardLabel.isUserInteractionEnabled = true
}
#IBAction func btnPressed(sender: UIButton) {
print("Button pressed")
buttonAction?(self)
}
When creating the cell, pass a block to it. That's the handler for the button.
When the button tapped, call the block.
You can save the block as a property of the subclass of UITableViewCell.
In your tableViewCell class, add a property:
class CustomTableViewCell: UITableViewCell {
open var completionHandler: (()->Void)?
}
In your viewController that has the tableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = CustomTableViewCell()
cell.completionHandler = {
() -> Void in
let newViewController = UIViewController()
//configure the VC here base on the indexPath
self.present(newViewController, animated: true, completion: nil)
}
return cell
}
I'm creating a sort of Contact list app w/ Xcode and everything was running smoothly until it came time for the app the actually add the new contact to the Contact list. What happens is that Once the user goes to the ViewController that helps the user create a new contact; the previously added Contact is removed from the list. I've tried to investigate and what I've discovered is that once I create a new Element (or Contact)for the Array; my code for some reason deletes the old Contact and replaces it with the newly created one. Whats also interesting is that by simply going to another page on my app (going to a new ViewController) my app for some reason removes the newly created Contact from the Contact list and leaves my TableView empty.
Here is a breakdown of my Coding and what I've tried:
this is the initial ViewController (I've implemented a TableView and created arrays that are organized in a cell within the TableView)
class ViewController: UIViewController, UITableViewDataSource,UITableViewDelegate {
#IBOutlet var TableView: UITableView!
var names = [String]()
var numbers = [String]()
override func viewDidAppear(animated: Bool) {
TableView.reloadData()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CustomCell
cell.Name.text = names[indexPath.row]
cell.PhoneNumber.text = numbers[indexPath.row]
cell.reloadInputViews()
return cell
}
}
This is Where my user will create a new Contact and how I try to append the new Contact to the contact list
classPickViewController: UIViewController,UIImagePickerControllerDelegate,UINavigationControllerDelegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if Save.touchInside{
var VC :ViewController = segue.destinationViewController as! ViewController
if VC.names.count >= 1 {
VC.names.insert(NameText, atIndex: (VC.names.count)+1)
VC.numbers.insert(PhoneText,atIndex: (VC.numbers.count)+1)
}
else{
VC.names.insert(NameText, atIndex: (VC.names.count)+0)
VC.numbers.insert(PhoneText,atIndex: (VC.numbers.count)+0)
}
}
}
#IBAction func SaveContact(sender: UIButton) {
performSegueWithIdentifier("SaveEm", sender: self)//this takes the usr to the Contact list
}
Here is the code Ive used to initialize my Cells in the TableView (UITableViewCell file)
class CustomCell: UITableViewCell {
#IBOutlet var Name: UILabel!
#IBOutlet var PhoneNumber: UILabel!
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
}
}
Your app is navigating from ViewController->classPickViewController->ViewController, so you have a new instance of ViewController which will start with a new empty array.
You should look into unwind segues so that you can move back to the presenting ViewController.
You also need to use something like Core Data to persist your data between executions of your app.
I would store an array of a "Contact" struct rather than having two arrays
reloadInputViews() is an overkill when you are already in the cellForRowAtIndexPath. remove that and put reloaddata appropriately. If needed put again when you know that the control flow is over.
So remove reloadInputViews(), and revisit each view of tableviewcell in cellForRowAtIndexPath instead.