I have a tableview cell which contains a specific button for showing alert sheet.
I've learned that button itself can't be pressed inside of table view cell. It must be called from a view controller.
So I`ve added a callback closure like so:
class FeedViewCell: UITableViewCell {
var callback : (() -> ())?
static let reuseIdentifier: String = "FeedTableViewCell"
lazy var menuButton: UIButton = {
let btn = UIButton()
btn.isUserInteractionEnabled = true
btn.addTarget(self, action: #selector(menuTapped), for: .touchUpInside)
return btn
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(menuButton)
}
#objc func menuTapped() {
print("menu tapped")
callback?()
}
I suspect it might be a problem with table view cell registration.. Pls let me know if that's not it. And in the view controller I did this:
class FeedViewController: UIViewController {
// some code...
tableView.register(FeedViewCell.self, forCellReuseIdentifier: FeedViewCell.reuseIdentifier)
}
extension FeedViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FeedViewCell.reuseIdentifier, for: indexPath) as! FeedViewCell
cell.callback = {
print("menu")
let actionSheet = UIAlertController(title: "", message: "", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: { action in
print("tap dismiss")
}))
actionSheet.addAction(UIAlertAction(title: "Follow", style: .default, handler: { action in
print("tap follow")
}))
self.present(actionSheet, animated: true, completion: nil)
}
return cell
}
}
So the main question is, why is the button not working? It doesn't even print "menu"
Thank you for all of your answers
This line is wrong.
addSubview(menuButton)
Instead:
contentView.addSubview(menuButton)
Try to add your button into cell's content view like this:
self.contentView.addSubview(self.menuButton)
Related
This question already has answers here:
Get button click inside UITableViewCell
(18 answers)
Closed 3 years ago.
I have a UITableView list with a button which says "Click Me!". I tried following this answer below: https://stackoverflow.com/a/53043358/7746248 to tie the button to an action, but that didn't work for unknown reasons.
I have checked other ways to tie a button to an event, I have had no luck.
import UIKit
class SampleTableViewCell: UITableViewCell {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var button: UIButton!
var tapCallback: (() -> Void)?
#IBAction func didTap(_ sender: Any) {
tapCallback?()
}
}
class TableViewController: UITableViewController {
var tableArray = ["New York", "Chicago", "North Island"]
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
self.clearsSelectionOnViewWillAppear = false
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.reloadData()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.tableArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleTableViewCell
// Configure the cell...
cell.selectionStyle = UITableViewCell.SelectionStyle.none
let names = self.tableArray[indexPath.row]
cell.name.text = names
cell.tapCallback = {
// do stuff
DispatchQueue.main.async {
let alert = UIAlertController(title: "title", message: "Button Clicked!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
}
return cell
}
}
Any other simple way to do this?
Add target inside cellForRowAt like below
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleTableViewCell
// Configure the cell...
cell.selectionStyle = UITableViewCell.SelectionStyle.none
let names = self.tableArray[indexPath.row]
cell.name.text = names
cell. button.addTarget(self, action: #selector(alertMethod), for: .touchUpInside)
return cell
}
#objc fileprivate func alertMethod() {
let alert = UIAlertController(title: "title", message: "Button Clicked!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
}
How can I change the title of an UIAlertAction when I click the button ?
I want to click that button and from "Enable" to make it "Disable" for example.
I spent a lot of time trying to achieve this but I can't manage to do it.
Here is a small Demo with my issue: https://github.com/tygruletz/ChangeTitleOfAlertAction
Here is my code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
tableView.tableFooterView = UIView()
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
showOptions()
}
func showOptions(){
var enable = "Enable"
let disable = "Disable"
let applyOn = UIAlertAction(title: enable, style: .default, handler: { (action: UIAlertAction!) in
enable = disable
})
let actionSheet = configureActionSheet()
actionSheet.addAction(applyOn)
self.present(actionSheet, animated: true, completion: nil)
}
func configureActionSheet() -> UIAlertController {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
actionSheet.addAction(cancel)
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad ){
actionSheet.popoverPresentationController?.sourceView = self.view
actionSheet.popoverPresentationController?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
actionSheet.popoverPresentationController?.permittedArrowDirections = []
}
return actionSheet
}
}
And here is a capture of screen:
Thank you if you try to help me !
Please follow below code:
Define property in your UIViewController
var selectedIndexPath:IndexPath!
Add argument in showOptions method
func showOptions(indexPath:IndexPath){
var status = "Enable"
if selectedIndexPath == indexPath{
status = "Disable"
}
let applyOn = UIAlertAction(title: status, style: .default, handler: { (action: UIAlertAction!) in
if self.selectedIndexPath == indexPath{
self.selectedIndexPath = nil
}else{
self.selectedIndexPath = indexPath
}
})
let actionSheet = configureActionSheet()
actionSheet.addAction(applyOn)
self.present(actionSheet, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
showOptions(indexPath: indexPath)
}
Note:
If you are going with this approach, Then you will never faced cell usability issue.
I have a subclass of UITableView in my iOS app (Swift 4, XCode 9). This table has one row and I want it to display an alert when it's clicked, get some input from the user, and then update a label (lblUserFromPrefs) in the table when the user clicks "OK" in the alert. Currently everything works fine except the label doesn't get updated. Here is the relevant code from my UITableView subclass:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Clicked section: \(indexPath.section) row: \(indexPath.row)")
let alert = UIAlertController(title: "Username", message: "what is your name", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.text = ""
}
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (_) in
let textField = alert!.textFields![0]
if let text = textField.text {
print("Text field: \(text)")
DispatchQueue.main.async {
self.lblUserFromPrefs.text = text
print("label updated")
}
}
}))
DispatchQueue.main.async {
self.present(alert, animated: true, completion: nil)
}
}
What happens when I run this is the label's text doesn't change when the alert closes but does change immediately when the table row is clicked again. I don't know why it waits until the row is clicked again to update the text. All the print statements print when I expect (including printing "label updated" immediately when the alert's OK button is pressed) and they print the right things. I know that when you're trying to update the UI from a closure in a background thread you have to use DispatchQueue.main.async {} but I'm not sure why it's not updating even though I am using the main thread. I have tried using DispatchQueue.main.async(execute: {}) and putting self.lblUserFromPrefs.setNeedsDisplay() directly after self.lblUserFromPrefs.text = "text". Any ideas what I'm doing wrong? Thanks!!
Add the [weak self] to the dispatch like this:
DispatchQueue.main.async { [weak self] in
}
Try something like this:
let alertController = UIAlertController.init(title: "Enter some text", message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
// Text field configuration
textField.placeholder = "Enter..."
textField.text = ""
}
alertController.addAction(UIAlertAction.init(title: "Ok", style: .default, handler: { (action) in
if (alertController.textFields?.first?.hasText)! {
if let text = alertController.textFields?.first?.text {
self.label.text = text // Update the value
}
} else {
self.label.text = "" // Default value
}
}))
self.present(alertController, animated: true, completion: nil) // Present alert controller
Here is the sample code for the behavior you want to achieve. Just include your alert controller code in didSelect and you should be good to go!
import UIKit
import PlaygroundSupport
class MyViewController : UITableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
if cell == nil {
cell = UITableViewCell()
}
cell!.textLabel?.text = "Hello World!"
return cell!
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
cell?.textLabel?.text = "Hello Universe!"
}
}
I am trying to call a UIAlertController from within my UITtableViewCell when my function is called. It gives me an error saying present is not available. I understand it's not within a ViewController. I am looking for an approach to access it.
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let tapGestureShareImageView = UITapGestureRecognizer(target: self, action: #selector(self.shareImageTouchUpInside))
shareImageView.addGestureRecognizer(tapGestureShareImageView)
shareImageView.isUserInteractionEnabled = true
}
#objc func shareImageTouchUpInside() {
showAction()
}
func showAction() {
let alertController = UIAlertController(title: "Action Sheet", message: "What do you like to do", preferredStyle: .alert)
let okButton = UIAlertAction(title: "Done", style: .default, handler: { (action) -> Void in
print("Ok button tapped")
})
let deleteButton = UIAlertAction(title: "Skip", style: .destructive, handler: { (action) -> Void in
print("Delete button tapped")
})
alertController.addAction(okButton)
alertController.addAction(deleteButton)
present(alertController, animated: true, completion: nil)
}
You can try to use delegate
protocol AlertShower{
func showAlert(TableCustomCell)
}
class TableCustomCell: UITableViewCell {
var delegate: AlertShower?
#IBAction func showClicked(_ sender: UIButton) {
self.delegate?.alertShower(sender:self)
}
}
in the VC
class viewController: UIViewController, AlertShower {
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = areaSettTable.dequeueReusableCell(withIdentifier:CellIdentifier1) as! TableCustomCell
cell.delegate = self
return cell
}
func showAlert(sender:TableCustomCell) {
// show alert here
}
}
Present is only available to ViewControllers. You are going to have to redirect the touch event to your view controller. The most common way of doing this would be having a delegate property in your UITableViewCell.
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID276
I ran into a similar problem myself when creating a custom activity indicator from a subclassed UIView. What I did was create the 'show' function (in the subclass) and pass in a UIViewController parameter, like so:
public func play(inView view: UIViewController) {
//Perform action here
view.present(alertController, animated: true, completion: nil)
}
Simply call it in your view controller like so:
CustomClass.play(inView: self)
Hopefully this helps!
I have a view and delete button in each row of the table viewenter image description here. I would like to click each to delete or view the item inside. I've added two button functions, but how to know when I click delete or view details row number 2 then it will remove or view the row 2?
#IBAction func deleteBtn(_ sender: Any) {
let refreshAlert = UIAlertController(title: "Message", message: "Are you sure you want to remove this item from the cart?", preferredStyle: UIAlertControllerStyle.alert)
refreshAlert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { (action: UIAlertAction!) in
}))
refreshAlert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (action: UIAlertAction!) in
self.removeCartAPI()
let buttonTag = (sender as AnyObject).tag
//self.navigationController?.popViewController(animated: true)
}))
present(refreshAlert, animated: true, completion: nil)
}
#IBAction func viewDetailsBtn(_ sender: Any) {
let vc: ParcelSendParcelSummaryViewController? = self.storyboard?.instantiateViewController(withIdentifier: "pSummaryVC") as?ParcelSendParcelSummaryViewController
self.navigationController?.pushViewController(vc!, animated: true)
}
I recommend the delegate pattern here, something like this:
protocol YourCellNameDelegate {
func didTapDelete(at cell: YourCellName)
func didTapView(at cell: YourCellName)
}
class YourCellName: UITableViewCell {
weak var delegate: YouCellNameDelegate?
...
#IBAction func didTouchDeleteButton(sender: Any) {
delegate?.didTapDelete(at: self)
}
// same for did tap view
}
class ParcelSendParcelSummaryViewController {
....
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(
withIdentifier: "Your identifier",
for: indexPath
) as? YourCellName {
cell.delegate = self
}
}
}
extension ParcelSendParcelSummaryViewController: YouCellNameDelegate {
func didTapDelete(at cell: YourCellName){
if let index = tableView.indexPathForCellView(cell: cell) {
tableView.beginUpdates()
tableView.deleteRows(at: index, with: .automatic)
// Make sure you delete the item from the data source here, between begin and end updates
tableView.endUpdates()
}
}
func didTapView(at cell: YourCellName) {
// same as above, with your view logic
}
}